diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..41cbebc --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,49 @@ +name: Differential ShellCheck + +on: + push: + branches: + - main + - 'rc/**' + paths: + - 'install.sh' + - 'podkop/files/usr/bin/**' + - 'podkop/files/usr/lib/**' + - '.github/workflows/shellcheck.yml' + pull_request: + branches: + - main + - 'rc/**' + paths: + - 'install.sh' + - 'podkop/files/usr/bin/**' + - 'podkop/files/usr/lib/**' + - '.github/workflows/shellcheck.yml' + +permissions: + contents: read + +jobs: + shellcheck: + name: Differential ShellCheck + runs-on: ubuntu-24.04 + + permissions: + contents: read + security-events: write + + steps: + - name: Checkout code + uses: actions/checkout@v5.0.0 + with: + fetch-depth: 0 + + - name: Differential ShellCheck + uses: redhat-plumbers-in-action/differential-shellcheck@v5.5.5 + with: + severity: error + include-path: | + podkop/files/usr/bin/podkop + podkop/files/usr/lib/**.sh + install.sh + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ff06e12..21b50a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea fe-app-podkop/node_modules fe-app-podkop/.env +.DS_Store diff --git a/README.md b/README.md index c7f40ee..a938bc1 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,24 @@ https://podkop.net/ sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) ``` +## Изменения 0.7.0 +Начиная с версии 0.7.0 изменена структура конфига `/etc/config/podkop`. Старые значения несовместимы с новыми. Нужно заново настроить Podkop. + +Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже. + +При обновлении вручную нужно: + +0. Не ныть в issue и чатик. +1. Забэкапить старый конфиг: +``` +mv /etc/config/podkop /etc/config/podkop-070 +``` +2. Стянуть новый дефолтный конфиг: +``` +wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop +``` +3. Настроить заново ваш Podkop через Luci или UCI. + # ToDo > [!IMPORTANT] diff --git a/String-example.md b/String-example.md index d694504..6e6b148 100644 --- a/String-example.md +++ b/String-example.md @@ -1,3 +1,11 @@ +## Socks +``` +socks4://127.0.0.1:1080 +socks4a://127.0.0.1:1080 +socks5://127.0.0.1:1080 +socks5://username:password@127.0.0.1:1080 +``` + ## Shadowsocks ``` ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client diff --git a/fe-app-podkop/.env.example b/fe-app-podkop/.env.example new file mode 100644 index 0000000..b82d0b6 --- /dev/null +++ b/fe-app-podkop/.env.example @@ -0,0 +1,16 @@ +SFTP_HOST=192.168.160.129 +SFTP_PORT=22 +SFTP_USER=root +SFTP_PASS= + +# you can use key if needed +# SFTP_PRIVATE_KEY=~/.ssh/id_rsa + +LOCAL_DIR_FE=../luci-app-podkop/htdocs/luci-static/resources/view/podkop +REMOTE_DIR_FE=/www/luci-static/resources/view/podkop + +LOCAL_DIR_BIN=../podkop/files/usr/bin/ +REMOTE_DIR_BIN=/usr/bin/ + +LOCAL_DIR_LIB=../podkop/files/usr/lib/ +REMOTE_DIR_LIB=/usr/lib/podkop/ diff --git a/fe-app-podkop/distribute-locales.js b/fe-app-podkop/distribute-locales.js new file mode 100644 index 0000000..0bc27b2 --- /dev/null +++ b/fe-app-podkop/distribute-locales.js @@ -0,0 +1,38 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const sourceDir = path.resolve(__dirname, 'locales'); +const targetRoot = path.resolve(__dirname, '../luci-app-podkop/po'); + +async function main() { + const files = await fs.readdir(sourceDir); + + for (const file of files) { + const filePath = path.join(sourceDir, file); + + if (file === 'podkop.pot') { + const potTarget = path.join(targetRoot, 'templates', 'podkop.pot'); + await fs.mkdir(path.dirname(potTarget), { recursive: true }); + await fs.copyFile(filePath, potTarget); + console.log(`✅ Copied POT: ${filePath} → ${potTarget}`); + } + + const match = file.match(/^podkop\.([a-zA-Z_]+)\.po$/); + if (match) { + const lang = match[1]; + const poTarget = path.join(targetRoot, lang, 'podkop.po'); + await fs.mkdir(path.dirname(poTarget), { recursive: true }); + await fs.copyFile(filePath, poTarget); + console.log(`✅ Copied ${lang.toUpperCase()}: ${filePath} → ${poTarget}`); + } + } +} + +main().catch((err) => { + console.error('❌ Ошибка при распространении переводов:', err); + process.exit(1); +}); diff --git a/fe-app-podkop/extract-calls.js b/fe-app-podkop/extract-calls.js new file mode 100644 index 0000000..6b7d765 --- /dev/null +++ b/fe-app-podkop/extract-calls.js @@ -0,0 +1,75 @@ +import fs from 'fs/promises'; +import path from 'path'; +import glob from 'fast-glob'; +import { parse } from '@babel/parser'; +import traverse from '@babel/traverse'; +import * as t from '@babel/types'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function stripIllegalReturn(code) { + return code.replace(/^\s*return\s+[^;]+;\s*$/gm, (match, offset, input) => { + const after = input.slice(offset + match.length).trim(); + return after === '' ? '' : match; + }); +} + +const files = await glob([ + 'src/**/*.ts', + '../luci-app-podkop/htdocs/luci-static/resources/view/podkop/**/*.js', +], { + ignore: [ + '**/*.test.ts', + '**/main.js', + '../luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js', + ], + absolute: true, +}); + +const results = {}; + +for (const file of files) { + const contentRaw = await fs.readFile(file, 'utf8'); + const content = stripIllegalReturn(contentRaw); + const relativePath = path.relative(process.cwd(), file); + + let ast; + try { + ast = parse(content, { + sourceType: 'module', + plugins: file.endsWith('.ts') ? ['typescript'] : [], + }); + } catch (e) { + console.warn(`⚠️ Parse error in ${relativePath}, skipping`); + continue; + } + + traverse.default(ast, { + CallExpression(path) { + if (t.isIdentifier(path.node.callee, { name: '_' })) { + const arg = path.node.arguments[0]; + if (t.isStringLiteral(arg)) { + const key = arg.value.trim(); + if (!key) return; // ❌ пропустить пустые ключи + const location = `${relativePath}:${path.node.loc?.start.line ?? '?'}`; + + if (!results[key]) { + results[key] = { call: key, key, places: [] }; + } + + results[key].places.push(location); + } + } + }, + }); +} + +const outFile = 'locales/calls.json'; +const sorted = Object.values(results).sort((a, b) => a.key.localeCompare(b.key)); // 🔤 сортировка по ключу + +await fs.mkdir(path.dirname(outFile), { recursive: true }); +await fs.writeFile(outFile, JSON.stringify(sorted, null, 2), 'utf8'); +console.log(`✅ Extracted ${sorted.length} translations to ${outFile}`); diff --git a/fe-app-podkop/generate-po.js b/fe-app-podkop/generate-po.js new file mode 100644 index 0000000..234d6b8 --- /dev/null +++ b/fe-app-podkop/generate-po.js @@ -0,0 +1,113 @@ +import fs from 'fs/promises'; +import { execSync } from 'child_process'; + +const lang = process.argv[2]; +if (!lang) { + console.error('❌ Укажи язык, например: node generate-po.js ru'); + process.exit(1); +} + +const callsPath = 'locales/calls.json'; +const poPath = `locales/podkop.${lang}.po`; + +function getGitUser() { + try { + return execSync('git config user.name').toString().trim(); + } catch { + return 'Automatically generated'; + } +} + +function getHeader(lang) { + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0].slice(0, 5); + const tzOffset = (() => { + const offset = -now.getTimezoneOffset(); + const sign = offset >= 0 ? '+' : '-'; + const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0'); + const minutes = String(Math.abs(offset) % 60).padStart(2, '0'); + return `${sign}${hours}${minutes}`; + })(); + + const translator = getGitUser(); + const pluralForms = lang === 'ru' + ? 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);' + : 'nplurals=2; plural=(n != 1);'; + + return [ + `# ${lang.toUpperCase()} translations for PODKOP package.`, + `# Copyright (C) ${now.getFullYear()} THE PODKOP'S COPYRIGHT HOLDER`, + `# This file is distributed under the same license as the PODKOP package.`, + `# ${translator}, ${now.getFullYear()}.`, + '#', + 'msgid ""', + 'msgstr ""', + `"Project-Id-Version: PODKOP\\n"`, + `"Report-Msgid-Bugs-To: \\n"`, + `"POT-Creation-Date: ${date} ${time}${tzOffset}\\n"`, + `"PO-Revision-Date: ${date} ${time}${tzOffset}\\n"`, + `"Last-Translator: ${translator}\\n"`, + `"Language-Team: none\\n"`, + `"Language: ${lang}\\n"`, + `"MIME-Version: 1.0\\n"`, + `"Content-Type: text/plain; charset=UTF-8\\n"`, + `"Content-Transfer-Encoding: 8bit\\n"`, + `"Plural-Forms: ${pluralForms}\\n"`, + '', + ]; +} + +function parsePo(content) { + const lines = content.split('\n'); + const translations = new Map(); + let msgid = null; + let msgstr = null; + for (const line of lines) { + if (line.startsWith('msgid ')) { + msgid = JSON.parse(line.slice(6)); + } else if (line.startsWith('msgstr ') && msgid !== null) { + msgstr = JSON.parse(line.slice(7)); + translations.set(msgid, msgstr); + msgid = null; + msgstr = null; + } + } + return translations; +} + +function escapePoString(str) { + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +async function generatePo() { + const [callsRaw, oldPoRaw] = await Promise.all([ + fs.readFile(callsPath, 'utf8'), + fs.readFile(poPath, 'utf8').catch(() => ''), + ]); + + const calls = JSON.parse(callsRaw); + const oldTranslations = parsePo(oldPoRaw); + const header = getHeader(lang); + + const body = calls + .map(({ key }) => { + const msgid = key; + const msgstr = oldTranslations.get(msgid) || ''; + return [ + `msgid "${escapePoString(msgid)}"`, + `msgstr "${escapePoString(msgstr)}"`, + '' + ].join('\n'); + }) + .join('\n'); + + const finalPo = header.join('\n') + '\n' + body; + + await fs.writeFile(poPath, finalPo, 'utf8'); + console.log(`✅ Файл ${poPath} успешно сгенерирован. Переведено ${[...oldTranslations.keys()].length}/${calls.length}`); +} + +generatePo().catch((err) => { + console.error('Ошибка генерации PO файла:', err); +}); diff --git a/fe-app-podkop/generate-pot.js b/fe-app-podkop/generate-pot.js new file mode 100644 index 0000000..b739044 --- /dev/null +++ b/fe-app-podkop/generate-pot.js @@ -0,0 +1,73 @@ +import fs from 'fs/promises'; +import { execSync } from 'child_process'; + +const inputFile = 'locales/calls.json'; +const outputFile = 'locales/podkop.pot'; +const projectId = 'PODKOP'; + +function getGitUser() { + const name = execSync('git config user.name').toString().trim(); + const email = execSync('git config user.email').toString().trim(); + return { name, email }; +} + +function getPotHeader({ name, email }) { + const now = new Date(); + const date = now.toISOString().replace('T', ' ').slice(0, 16); + const offset = -now.getTimezoneOffset(); + const sign = offset >= 0 ? '+' : '-'; + const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0'); + const minutes = String(Math.abs(offset) % 60).padStart(2, '0'); + const timezone = `${sign}${hours}${minutes}`; + + return [ + '# SOME DESCRIPTIVE TITLE.', + `# Copyright (C) ${now.getFullYear()} THE PACKAGE'S COPYRIGHT HOLDER`, + `# This file is distributed under the same license as the ${projectId} package.`, + `# ${name} <${email}>, ${now.getFullYear()}.`, + '#, fuzzy', + 'msgid ""', + 'msgstr ""', + `"Project-Id-Version: ${projectId}\\n"`, + `"Report-Msgid-Bugs-To: \\n"`, + `"POT-Creation-Date: ${date}${timezone}\\n"`, + `"PO-Revision-Date: ${date}${timezone}\\n"`, + `"Last-Translator: ${name} <${email}>\\n"`, + `"Language-Team: LANGUAGE \\n"`, + `"Language: \\n"`, + `"MIME-Version: 1.0\\n"`, + `"Content-Type: text/plain; charset=UTF-8\\n"`, + `"Content-Transfer-Encoding: 8bit\\n"`, + '', + ].join('\n'); +} + +function escapePoString(str) { + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function generateEntry(item) { + const locations = item.places.map(loc => `#: ${loc}`).join('\n'); + const msgid = escapePoString(item.key); + return [ + locations, + `msgid "${msgid}"`, + `msgstr ""`, + '' + ].join('\n'); +} + +async function generatePot() { + const gitUser = getGitUser(); + const raw = await fs.readFile(inputFile, 'utf8'); + const entries = JSON.parse(raw); + + const header = getPotHeader(gitUser); + const body = entries.map(generateEntry).join('\n'); + + await fs.writeFile(outputFile, `${header}\n${body}`, 'utf8'); + + console.log(`✅ POT-файл успешно создан: ${outputFile}`); +} + +generatePot().catch(console.error); diff --git a/fe-app-podkop/locales/calls.json b/fe-app-podkop/locales/calls.json new file mode 100644 index 0000000..a5c991b --- /dev/null +++ b/fe-app-podkop/locales/calls.json @@ -0,0 +1,1665 @@ +[ + { + "call": "✔ Enabled", + "key": "✔ Enabled", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:342" + ] + }, + { + "call": "✔ Running", + "key": "✔ Running", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:353" + ] + }, + { + "call": "✘ Disabled", + "key": "✘ Disabled", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:343" + ] + }, + { + "call": "✘ Stopped", + "key": "✘ Stopped", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:354" + ] + }, + { + "call": "Active Connections", + "key": "Active Connections", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:304" + ] + }, + { + "call": "Additional marking rules found", + "key": "Additional marking rules found", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:117" + ] + }, + { + "call": "Applicable for SOCKS and Shadowsocks proxy", + "key": "Applicable for SOCKS and Shadowsocks proxy", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111" + ] + }, + { + "call": "At least one valid domain must be specified. Comments-only content is not allowed.", + "key": "At least one valid domain must be specified. Comments-only content is not allowed.", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:356" + ] + }, + { + "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", + "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:437" + ] + }, + { + "call": "Bootsrap DNS", + "key": "Bootsrap DNS", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72" + ] + }, + { + "call": "Bootstrap DNS server", + "key": "Bootstrap DNS server", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45" + ] + }, + { + "call": "Browser is not using FakeIP", + "key": "Browser is not using FakeIP", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:81" + ] + }, + { + "call": "Browser is using FakeIP correctly", + "key": "Browser is using FakeIP correctly", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:80" + ] + }, + { + "call": "Cache File Path", + "key": "Cache File Path", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:322" + ] + }, + { + "call": "Cache file path cannot be empty", + "key": "Cache file path cannot be empty", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336" + ] + }, + { + "call": "Cannot receive DNS checks result", + "key": "Cannot receive DNS checks result", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:26" + ] + }, + { + "call": "Cannot receive nftables checks result", + "key": "Cannot receive nftables checks result", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:27" + ] + }, + { + "call": "Cannot receive Sing-box checks result", + "key": "Cannot receive Sing-box checks result", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:24" + ] + }, + { + "call": "Checking dns, please wait", + "key": "Checking dns, please wait", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:14" + ] + }, + { + "call": "Checking FakeIP, please wait", + "key": "Checking FakeIP, please wait", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:14" + ] + }, + { + "call": "Checking nftables, please wait", + "key": "Checking nftables, please wait", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:12" + ] + }, + { + "call": "Checking sing-box, please wait", + "key": "Checking sing-box, please wait", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:12" + ] + }, + { + "call": "CIDR must be between 0 and 32", + "key": "CIDR must be between 0 and 32", + "places": [ + "src/validators/validateSubnet.ts:33" + ] + }, + { + "call": "Close", + "key": "Close", + "places": [ + "src/partials/modal/renderModal.ts:26" + ] + }, + { + "call": "Community Lists", + "key": "Community Lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:211" + ] + }, + { + "call": "Config File Path", + "key": "Config File Path", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:309" + ] + }, + { + "call": "Configuration for Podkop service", + "key": "Configuration for Podkop service", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27" + ] + }, + { + "call": "Configuration Type", + "key": "Configuration Type", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:22" + ] + }, + { + "call": "Connection Type", + "key": "Connection Type", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12" + ] + }, + { + "call": "Connection URL", + "key": "Connection URL", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:25" + ] + }, + { + "call": "Copy", + "key": "Copy", + "places": [ + "src/partials/modal/renderModal.ts:20" + ] + }, + { + "call": "Currently unavailable", + "key": "Currently unavailable", + "places": [ + "src/podkop/tabs/dashboard/partials/renderWidget.ts:22" + ] + }, + { + "call": "Dashboard", + "key": "Dashboard", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80" + ] + }, + { + "call": "Dashboard currently unavailable", + "key": "Dashboard currently unavailable", + "places": [ + "src/podkop/tabs/dashboard/partials/renderSections.ts:19" + ] + }, + { + "call": "Delay in milliseconds before reloading podkop after interface UP", + "key": "Delay in milliseconds before reloading podkop after interface UP", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:215" + ] + }, + { + "call": "Delay value cannot be empty", + "key": "Delay value cannot be empty", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222" + ] + }, + { + "call": "DHCP has DNS server", + "key": "DHCP has DNS server", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:89" + ] + }, + { + "call": "Diagnostics", + "key": "Diagnostics", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65" + ] + }, + { + "call": "Disable autostart", + "key": "Disable autostart", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79" + ] + }, + { + "call": "Disable QUIC", + "key": "Disable QUIC", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:239" + ] + }, + { + "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", + "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:240" + ] + }, + { + "call": "Disabled", + "key": "Disabled", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:302", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:382" + ] + }, + { + "call": "DNS checks", + "key": "DNS checks", + "places": [ + "src/podkop/tabs/diagnostic/checks/contstants.ts:14" + ] + }, + { + "call": "DNS checks passed", + "key": "DNS checks passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:64" + ] + }, + { + "call": "DNS on router", + "key": "DNS on router", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:84" + ] + }, + { + "call": "DNS over HTTPS (DoH)", + "key": "DNS over HTTPS (DoH)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:179", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15" + ] + }, + { + "call": "DNS over TLS (DoT)", + "key": "DNS over TLS (DoT)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:180", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16" + ] + }, + { + "call": "DNS Protocol Type", + "key": "DNS Protocol Type", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:176", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12" + ] + }, + { + "call": "DNS Rewrite TTL", + "key": "DNS Rewrite TTL", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68" + ] + }, + { + "call": "DNS Server", + "key": "DNS Server", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:189", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24" + ] + }, + { + "call": "DNS server address cannot be empty", + "key": "DNS server address cannot be empty", + "places": [ + "src/validators/validateDns.ts:7" + ] + }, + { + "call": "Domain Resolver", + "key": "Domain Resolver", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:166" + ] + }, + { + "call": "Dont Touch My DHCP!", + "key": "Dont Touch My DHCP!", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:300" + ] + }, + { + "call": "Downlink", + "key": "Downlink", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:238", + "src/podkop/tabs/dashboard/initController.ts:272" + ] + }, + { + "call": "Download", + "key": "Download", + "places": [ + "src/partials/modal/renderModal.ts:15" + ] + }, + { + "call": "Download Lists via Proxy/VPN", + "key": "Download Lists via Proxy/VPN", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:262" + ] + }, + { + "call": "Download Lists via specific proxy section", + "key": "Download Lists via specific proxy section", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:271" + ] + }, + { + "call": "Downloading all lists via main Proxy/VPN", + "key": "Downloading all lists via main Proxy/VPN", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:263" + ] + }, + { + "call": "Downloading all lists via specific Proxy/VPN", + "key": "Downloading all lists via specific Proxy/VPN", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:272" + ] + }, + { + "call": "Dynamic List", + "key": "Dynamic List", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:303", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:383" + ] + }, + { + "call": "Enable autostart", + "key": "Enable autostart", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89" + ] + }, + { + "call": "Enable built-in DNS resolver for domains handled by this section", + "key": "Enable built-in DNS resolver for domains handled by this section", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:167" + ] + }, + { + "call": "Enable Mixed Proxy", + "key": "Enable Mixed Proxy", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:575" + ] + }, + { + "call": "Enable Output Network Interface", + "key": "Enable Output Network Interface", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126" + ] + }, + { + "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", + "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:576" + ] + }, + { + "call": "Enable YACD", + "key": "Enable YACD", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:230" + ] + }, + { + "call": "Enter complete outbound configuration in JSON format", + "key": "Enter complete outbound configuration in JSON format", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65" + ] + }, + { + "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", + "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:338" + ] + }, + { + "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", + "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:312" + ] + }, + { + "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", + "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392" + ] + }, + { + "call": "Exclude NTP", + "key": "Exclude NTP", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:358" + ] + }, + { + "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", + "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:359" + ] + }, + { + "call": "Failed to copy!", + "key": "Failed to copy!", + "places": [ + "src/helpers/copyToClipboard.ts:12" + ] + }, + { + "call": "FakeIP checks", + "key": "FakeIP checks", + "places": [ + "src/podkop/tabs/diagnostic/checks/contstants.ts:29" + ] + }, + { + "call": "FakeIP checks failed", + "key": "FakeIP checks failed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57" + ] + }, + { + "call": "FakeIP checks partially passed", + "key": "FakeIP checks partially passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51" + ] + }, + { + "call": "FakeIP checks passed", + "key": "FakeIP checks passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:44" + ] + }, + { + "call": "Fastest", + "key": "Fastest", + "places": [ + "src/podkop/methods/custom/getDashboardSections.ts:117" + ] + }, + { + "call": "Fully Routed IPs", + "key": "Fully Routed IPs", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:550" + ] + }, + { + "call": "Get global check", + "key": "Get global check", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98" + ] + }, + { + "call": "Global check", + "key": "Global check", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:218" + ] + }, + { + "call": "HTTP error", + "key": "HTTP error", + "places": [ + "src/podkop/api.ts:27" + ] + }, + { + "call": "Interface Monitoring", + "key": "Interface Monitoring", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:182" + ] + }, + { + "call": "Interface Monitoring Delay", + "key": "Interface Monitoring Delay", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:214" + ] + }, + { + "call": "Interface monitoring for Bad WAN", + "key": "Interface monitoring for Bad WAN", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:183" + ] + }, + { + "call": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", + "key": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", + "places": [ + "src/validators/validateDns.ts:20" + ] + }, + { + "call": "Invalid domain address", + "key": "Invalid domain address", + "places": [ + "src/validators/validateDomain.ts:18", + "src/validators/validateDomain.ts:27" + ] + }, + { + "call": "Invalid format. Use X.X.X.X or X.X.X.X/Y", + "key": "Invalid format. Use X.X.X.X or X.X.X.X/Y", + "places": [ + "src/validators/validateSubnet.ts:11" + ] + }, + { + "call": "Invalid IP address", + "key": "Invalid IP address", + "places": [ + "src/validators/validateIp.ts:11" + ] + }, + { + "call": "Invalid JSON format", + "key": "Invalid JSON format", + "places": [ + "src/validators/validateOutboundJson.ts:19" + ] + }, + { + "call": "Invalid path format. Path must start with \"/\" and contain valid characters", + "key": "Invalid path format. Path must start with \"/\" and contain valid characters", + "places": [ + "src/validators/validatePath.ts:22" + ] + }, + { + "call": "Invalid port number. Must be between 1 and 65535", + "key": "Invalid port number. Must be between 1 and 65535", + "places": [ + "src/validators/validateShadowsocksUrl.ts:85" + ] + }, + { + "call": "Invalid Shadowsocks URL: decoded credentials must contain method:password", + "key": "Invalid Shadowsocks URL: decoded credentials must contain method:password", + "places": [ + "src/validators/validateShadowsocksUrl.ts:37" + ] + }, + { + "call": "Invalid Shadowsocks URL: missing credentials", + "key": "Invalid Shadowsocks URL: missing credentials", + "places": [ + "src/validators/validateShadowsocksUrl.ts:27" + ] + }, + { + "call": "Invalid Shadowsocks URL: missing method and password separator \":\"", + "key": "Invalid Shadowsocks URL: missing method and password separator \":\"", + "places": [ + "src/validators/validateShadowsocksUrl.ts:46" + ] + }, + { + "call": "Invalid Shadowsocks URL: missing port", + "key": "Invalid Shadowsocks URL: missing port", + "places": [ + "src/validators/validateShadowsocksUrl.ts:76" + ] + }, + { + "call": "Invalid Shadowsocks URL: missing server", + "key": "Invalid Shadowsocks URL: missing server", + "places": [ + "src/validators/validateShadowsocksUrl.ts:67" + ] + }, + { + "call": "Invalid Shadowsocks URL: missing server address", + "key": "Invalid Shadowsocks URL: missing server address", + "places": [ + "src/validators/validateShadowsocksUrl.ts:58" + ] + }, + { + "call": "Invalid Shadowsocks URL: must not contain spaces", + "key": "Invalid Shadowsocks URL: must not contain spaces", + "places": [ + "src/validators/validateShadowsocksUrl.ts:16" + ] + }, + { + "call": "Invalid Shadowsocks URL: must start with ss://", + "key": "Invalid Shadowsocks URL: must start with ss://", + "places": [ + "src/validators/validateShadowsocksUrl.ts:8" + ] + }, + { + "call": "Invalid Shadowsocks URL: parsing failed", + "key": "Invalid Shadowsocks URL: parsing failed", + "places": [ + "src/validators/validateShadowsocksUrl.ts:91" + ] + }, + { + "call": "Invalid SOCKS URL: invalid host format", + "key": "Invalid SOCKS URL: invalid host format", + "places": [ + "src/validators/validateSocksUrl.ts:73" + ] + }, + { + "call": "Invalid SOCKS URL: invalid port number", + "key": "Invalid SOCKS URL: invalid port number", + "places": [ + "src/validators/validateSocksUrl.ts:63" + ] + }, + { + "call": "Invalid SOCKS URL: missing host and port", + "key": "Invalid SOCKS URL: missing host and port", + "places": [ + "src/validators/validateSocksUrl.ts:42" + ] + }, + { + "call": "Invalid SOCKS URL: missing hostname or IP", + "key": "Invalid SOCKS URL: missing hostname or IP", + "places": [ + "src/validators/validateSocksUrl.ts:51" + ] + }, + { + "call": "Invalid SOCKS URL: missing port", + "key": "Invalid SOCKS URL: missing port", + "places": [ + "src/validators/validateSocksUrl.ts:56" + ] + }, + { + "call": "Invalid SOCKS URL: missing username", + "key": "Invalid SOCKS URL: missing username", + "places": [ + "src/validators/validateSocksUrl.ts:34" + ] + }, + { + "call": "Invalid SOCKS URL: must not contain spaces", + "key": "Invalid SOCKS URL: must not contain spaces", + "places": [ + "src/validators/validateSocksUrl.ts:19" + ] + }, + { + "call": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", + "key": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", + "places": [ + "src/validators/validateSocksUrl.ts:10" + ] + }, + { + "call": "Invalid SOCKS URL: parsing failed", + "key": "Invalid SOCKS URL: parsing failed", + "places": [ + "src/validators/validateSocksUrl.ts:77" + ] + }, + { + "call": "Invalid Trojan URL: must not contain spaces", + "key": "Invalid Trojan URL: must not contain spaces", + "places": [ + "src/validators/validateTrojanUrl.ts:15" + ] + }, + { + "call": "Invalid Trojan URL: must start with trojan://", + "key": "Invalid Trojan URL: must start with trojan://", + "places": [ + "src/validators/validateTrojanUrl.ts:8" + ] + }, + { + "call": "Invalid Trojan URL: parsing failed", + "key": "Invalid Trojan URL: parsing failed", + "places": [ + "src/validators/validateTrojanUrl.ts:56" + ] + }, + { + "call": "Invalid URL format", + "key": "Invalid URL format", + "places": [ + "src/validators/validateUrl.ts:18" + ] + }, + { + "call": "Invalid VLESS URL: parsing failed", + "key": "Invalid VLESS URL: parsing failed", + "places": [ + "src/validators/validateVlessUrl.ts:109" + ] + }, + { + "call": "IP address 0.0.0.0 is not allowed", + "key": "IP address 0.0.0.0 is not allowed", + "places": [ + "src/validators/validateSubnet.ts:18" + ] + }, + { + "call": "Latest", + "key": "Latest", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:404" + ] + }, + { + "call": "List Update Frequency", + "key": "List Update Frequency", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:250" + ] + }, + { + "call": "Local Domain Lists", + "key": "Local Domain Lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458" + ] + }, + { + "call": "Local Subnet Lists", + "key": "Local Subnet Lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:481" + ] + }, + { + "call": "Main DNS", + "key": "Main DNS", + "places": [ + "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:79" + ] + }, + { + "call": "Memory Usage", + "key": "Memory Usage", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:308" + ] + }, + { + "call": "Mixed Proxy Port", + "key": "Mixed Proxy Port", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:586" + ] + }, + { + "call": "Monitored Interfaces", + "key": "Monitored Interfaces", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:191" + ] + }, + { + "call": "Network Interface", + "key": "Network Interface", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:120" + ] + }, + { + "call": "Nftables checks", + "key": "Nftables checks", + "places": [ + "src/podkop/tabs/diagnostic/checks/contstants.ts:24" + ] + }, + { + "call": "Nftables checks partially passed", + "key": "Nftables checks partially passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:75" + ] + }, + { + "call": "Nftables checks passed", + "key": "Nftables checks passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74" + ] + }, + { + "call": "No other marking rules found", + "key": "No other marking rules found", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:116" + ] + }, + { + "call": "Not implement yet", + "key": "Not implement yet", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189" + ] + }, + { + "call": "Not running", + "key": "Not running", + "places": [ + "src/podkop/tabs/diagnostic/diagnostic.store.ts:55", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:63", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:71", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:79" + ] + }, + { + "call": "Operation timed out", + "key": "Operation timed out", + "places": [ + "src/helpers/withTimeout.ts:7" + ] + }, + { + "call": "Outbound Config", + "key": "Outbound Config", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26" + ] + }, + { + "call": "Outbound Configuration", + "key": "Outbound Configuration", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:64" + ] + }, + { + "call": "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields", + "key": "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields", + "places": [ + "src/validators/validateOutboundJson.ts:11" + ] + }, + { + "call": "Outdated", + "key": "Outdated", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:394" + ] + }, + { + "call": "Output Network Interface", + "key": "Output Network Interface", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135" + ] + }, + { + "call": "Path cannot be empty", + "key": "Path cannot be empty", + "places": [ + "src/validators/validatePath.ts:7" + ] + }, + { + "call": "Path must be absolute (start with /)", + "key": "Path must be absolute (start with /)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:340" + ] + }, + { + "call": "Path must contain at least one directory (like /tmp/cache.db)", + "key": "Path must contain at least one directory (like /tmp/cache.db)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349" + ] + }, + { + "call": "Path must end with cache.db", + "key": "Path must end with cache.db", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:344" + ] + }, + { + "call": "Podkop", + "key": "Podkop", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:340" + ] + }, + { + "call": "Podkop Settings", + "key": "Podkop Settings", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26" + ] + }, + { + "call": "Podkop will not modify your DHCP configuration", + "key": "Podkop will not modify your DHCP configuration", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:301" + ] + }, + { + "call": "Proxy Configuration URL", + "key": "Proxy Configuration URL", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:34" + ] + }, + { + "call": "Proxy traffic is not routed via FakeIP", + "key": "Proxy traffic is not routed via FakeIP", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:89" + ] + }, + { + "call": "Proxy traffic is routed via FakeIP", + "key": "Proxy traffic is routed via FakeIP", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:88" + ] + }, + { + "call": "Queued", + "key": "Queued", + "places": [ + "src/podkop/tabs/diagnostic/diagnostic.store.ts:95", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:103", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:111", + "src/podkop/tabs/diagnostic/diagnostic.store.ts:119" + ] + }, + { + "call": "Regional options cannot be used together", + "key": "Regional options cannot be used together", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:245" + ] + }, + { + "call": "Remote Domain Lists", + "key": "Remote Domain Lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:504" + ] + }, + { + "call": "Remote Subnet Lists", + "key": "Remote Subnet Lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:527" + ] + }, + { + "call": "Restart podkop", + "key": "Restart podkop", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49" + ] + }, + { + "call": "Router DNS is not routed through sing-box", + "key": "Router DNS is not routed through sing-box", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:74" + ] + }, + { + "call": "Router DNS is routed through sing-box", + "key": "Router DNS is routed through sing-box", + "places": [ + "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:73" + ] + }, + { + "call": "Routing Excluded IPs", + "key": "Routing Excluded IPs", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:369" + ] + }, + { + "call": "Rules mangle counters", + "key": "Rules mangle counters", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:90" + ] + }, + { + "call": "Rules mangle exist", + "key": "Rules mangle exist", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:85" + ] + }, + { + "call": "Rules mangle output counters", + "key": "Rules mangle output counters", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:100" + ] + }, + { + "call": "Rules mangle output exist", + "key": "Rules mangle output exist", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:95" + ] + }, + { + "call": "Rules proxy counters", + "key": "Rules proxy counters", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:110" + ] + }, + { + "call": "Rules proxy exist", + "key": "Rules proxy exist", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105" + ] + }, + { + "call": "Run Diagnostic", + "key": "Run Diagnostic", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15" + ] + }, + { + "call": "Russia inside restrictions", + "key": "Russia inside restrictions", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264" + ] + }, + { + "call": "Sections", + "key": "Sections", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36" + ] + }, + { + "call": "Select a predefined list for routing", + "key": "Select a predefined list for routing", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:212" + ] + }, + { + "call": "Select between VPN and Proxy connection methods for traffic routing", + "key": "Select between VPN and Proxy connection methods for traffic routing", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13" + ] + }, + { + "call": "Select DNS protocol to use", + "key": "Select DNS protocol to use", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13" + ] + }, + { + "call": "Select how often the domain or subnet lists are updated automatically", + "key": "Select how often the domain or subnet lists are updated automatically", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:251" + ] + }, + { + "call": "Select how to configure the proxy", + "key": "Select how to configure the proxy", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23" + ] + }, + { + "call": "Select network interface for VPN connection", + "key": "Select network interface for VPN connection", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:121" + ] + }, + { + "call": "Select or enter DNS server address", + "key": "Select or enter DNS server address", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:190", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25" + ] + }, + { + "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", + "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:323" + ] + }, + { + "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", + "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:310" + ] + }, + { + "call": "Select the DNS protocol type for the domain resolver", + "key": "Select the DNS protocol type for the domain resolver", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:177" + ] + }, + { + "call": "Select the list type for adding custom domains", + "key": "Select the list type for adding custom domains", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300" + ] + }, + { + "call": "Select the list type for adding custom subnets", + "key": "Select the list type for adding custom subnets", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:380" + ] + }, + { + "call": "Select the network interface from which the traffic will originate", + "key": "Select the network interface from which the traffic will originate", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90" + ] + }, + { + "call": "Select the network interface to which the traffic will originate", + "key": "Select the network interface to which the traffic will originate", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136" + ] + }, + { + "call": "Select the WAN interfaces to be monitored", + "key": "Select the WAN interfaces to be monitored", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:192" + ] + }, + { + "call": "Services info", + "key": "Services info", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:337" + ] + }, + { + "call": "Settings", + "key": "Settings", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49" + ] + }, + { + "call": "Show sing-box config", + "key": "Show sing-box config", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:278", + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116" + ] + }, + { + "call": "Sing-box", + "key": "Sing-box", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:351" + ] + }, + { + "call": "Sing-box autostart disabled", + "key": "Sing-box autostart disabled", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:86" + ] + }, + { + "call": "Sing-box checks", + "key": "Sing-box checks", + "places": [ + "src/podkop/tabs/diagnostic/checks/contstants.ts:19" + ] + }, + { + "call": "Sing-box checks passed", + "key": "Sing-box checks passed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:66" + ] + }, + { + "call": "Sing-box installed", + "key": "Sing-box installed", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:71" + ] + }, + { + "call": "Sing-box listening ports", + "key": "Sing-box listening ports", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:96" + ] + }, + { + "call": "Sing-box process running", + "key": "Sing-box process running", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:91" + ] + }, + { + "call": "Sing-box service exist", + "key": "Sing-box service exist", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:81" + ] + }, + { + "call": "Sing-box version >= 1.12.4", + "key": "Sing-box version >= 1.12.4", + "places": [ + "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:76" + ] + }, + { + "call": "Source Network Interface", + "key": "Source Network Interface", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89" + ] + }, + { + "call": "Specify a local IP address to be excluded from routing", + "key": "Specify a local IP address to be excluded from routing", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370" + ] + }, + { + "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", + "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:551" + ] + }, + { + "call": "Specify remote URLs to download and use domain lists", + "key": "Specify remote URLs to download and use domain lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505" + ] + }, + { + "call": "Specify remote URLs to download and use subnet lists", + "key": "Specify remote URLs to download and use subnet lists", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:528" + ] + }, + { + "call": "Specify the path to the list file located on the router filesystem", + "key": "Specify the path to the list file located on the router filesystem", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:459", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:482" + ] + }, + { + "call": "Start podkop", + "key": "Start podkop", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69" + ] + }, + { + "call": "Stop podkop", + "key": "Stop podkop", + "places": [ + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59" + ] + }, + { + "call": "Successfully copied!", + "key": "Successfully copied!", + "places": [ + "src/helpers/copyToClipboard.ts:10" + ] + }, + { + "call": "System info", + "key": "System info", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:301" + ] + }, + { + "call": "Table exist", + "key": "Table exist", + "places": [ + "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:80" + ] + }, + { + "call": "Test latency", + "key": "Test latency", + "places": [ + "src/podkop/tabs/dashboard/partials/renderSections.ts:108" + ] + }, + { + "call": "Text List", + "key": "Text List", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:304" + ] + }, + { + "call": "Text List (comma/space/newline separated)", + "key": "Text List (comma/space/newline separated)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:384" + ] + }, + { + "call": "The DNS server used to look up the IP address of an upstream DNS server", + "key": "The DNS server used to look up the IP address of an upstream DNS server", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46" + ] + }, + { + "call": "Time in seconds for DNS record caching (default: 60)", + "key": "Time in seconds for DNS record caching (default: 60)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69" + ] + }, + { + "call": "Traffic", + "key": "Traffic", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:235" + ] + }, + { + "call": "Traffic Total", + "key": "Traffic Total", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:265" + ] + }, + { + "call": "TTL must be a positive number", + "key": "TTL must be a positive number", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80" + ] + }, + { + "call": "TTL value cannot be empty", + "key": "TTL value cannot be empty", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75" + ] + }, + { + "call": "UDP (Unprotected DNS)", + "key": "UDP (Unprotected DNS)", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:181", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17" + ] + }, + { + "call": "UDP over TCP", + "key": "UDP over TCP", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:110" + ] + }, + { + "call": "unknown", + "key": "unknown", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:34", + "src/podkop/tabs/diagnostic/initController.ts:35", + "src/podkop/tabs/diagnostic/initController.ts:36", + "src/podkop/tabs/diagnostic/initController.ts:37", + "src/podkop/tabs/diagnostic/initController.ts:38", + "src/podkop/tabs/diagnostic/initController.ts:39", + "src/podkop/tabs/diagnostic/initController.ts:373" + ] + }, + { + "call": "Unknown error", + "key": "Unknown error", + "places": [ + "src/podkop/api.ts:40" + ] + }, + { + "call": "Uplink", + "key": "Uplink", + "places": [ + "src/podkop/tabs/dashboard/initController.ts:237", + "src/podkop/tabs/dashboard/initController.ts:268" + ] + }, + { + "call": "URL must start with vless://, ss://, trojan://, or socks4/5://", + "key": "URL must start with vless://, ss://, trojan://, or socks4/5://", + "places": [ + "src/validators/validateProxyUrl.ts:27" + ] + }, + { + "call": "URL must use one of the following protocols:", + "key": "URL must use one of the following protocols:", + "places": [ + "src/validators/validateUrl.ts:13" + ] + }, + { + "call": "URLTest", + "key": "URLTest", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27" + ] + }, + { + "call": "URLTest Proxy Links", + "key": "URLTest Proxy Links", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:87" + ] + }, + { + "call": "User Domain List Type", + "key": "User Domain List Type", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299" + ] + }, + { + "call": "User Domains", + "key": "User Domains", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:311" + ] + }, + { + "call": "User Domains List", + "key": "User Domains List", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:337" + ] + }, + { + "call": "User Subnet List Type", + "key": "User Subnet List Type", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:379" + ] + }, + { + "call": "User Subnets", + "key": "User Subnets", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391" + ] + }, + { + "call": "User Subnets List", + "key": "User Subnets List", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:417" + ] + }, + { + "call": "Valid", + "key": "Valid", + "places": [ + "src/validators/validateDns.ts:11", + "src/validators/validateDns.ts:15", + "src/validators/validateDomain.ts:13", + "src/validators/validateDomain.ts:30", + "src/validators/validateIp.ts:8", + "src/validators/validateOutboundJson.ts:17", + "src/validators/validatePath.ts:16", + "src/validators/validateShadowsocksUrl.ts:95", + "src/validators/validateSocksUrl.ts:80", + "src/validators/validateSubnet.ts:38", + "src/validators/validateTrojanUrl.ts:59", + "src/validators/validateUrl.ts:16", + "src/validators/validateVlessUrl.ts:107" + ] + }, + { + "call": "Validation errors:", + "key": "Validation errors:", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:370", + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:449" + ] + }, + { + "call": "View logs", + "key": "View logs", + "places": [ + "src/podkop/tabs/diagnostic/initController.ts:248", + "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107" + ] + }, + { + "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", + "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:247" + ] + }, + { + "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", + "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:266" + ] + }, + { + "call": "You can select Output Network Interface, by default autodetect", + "key": "You can select Output Network Interface, by default autodetect", + "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127" + ] + } +] \ No newline at end of file diff --git a/fe-app-podkop/locales/podkop.pot b/fe-app-podkop/locales/podkop.pot new file mode 100644 index 0000000..75fe1bb --- /dev/null +++ b/fe-app-podkop/locales/podkop.pot @@ -0,0 +1,984 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PODKOP package. +# divocat , 2025. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PODKOP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-21 20:02+0300\n" +"PO-Revision-Date: 2025-10-21 20:02+0300\n" +"Last-Translator: divocat \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/podkop/tabs/dashboard/initController.ts:342 +msgid "✔ Enabled" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:353 +msgid "✔ Running" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:343 +msgid "✘ Disabled" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:354 +msgid "✘ Stopped" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:304 +msgid "Active Connections" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:117 +msgid "Additional marking rules found" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111 +msgid "Applicable for SOCKS and Shadowsocks proxy" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:356 +msgid "At least one valid domain must be specified. Comments-only content is not allowed." +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:437 +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72 +msgid "Bootsrap DNS" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45 +msgid "Bootstrap DNS server" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:81 +msgid "Browser is not using FakeIP" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:80 +msgid "Browser is using FakeIP correctly" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:322 +msgid "Cache File Path" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336 +msgid "Cache file path cannot be empty" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:26 +msgid "Cannot receive DNS checks result" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:27 +msgid "Cannot receive nftables checks result" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:24 +msgid "Cannot receive Sing-box checks result" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:14 +msgid "Checking dns, please wait" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:14 +msgid "Checking FakeIP, please wait" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:12 +msgid "Checking nftables, please wait" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:12 +msgid "Checking sing-box, please wait" +msgstr "" + +#: src/validators/validateSubnet.ts:33 +msgid "CIDR must be between 0 and 32" +msgstr "" + +#: src/partials/modal/renderModal.ts:26 +msgid "Close" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:211 +msgid "Community Lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:309 +msgid "Config File Path" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27 +msgid "Configuration for Podkop service" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:22 +msgid "Configuration Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12 +msgid "Connection Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:25 +msgid "Connection URL" +msgstr "" + +#: src/partials/modal/renderModal.ts:20 +msgid "Copy" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22 +msgid "Currently unavailable" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80 +msgid "Dashboard" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderSections.ts:19 +msgid "Dashboard currently unavailable" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:215 +msgid "Delay in milliseconds before reloading podkop after interface UP" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222 +msgid "Delay value cannot be empty" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:89 +msgid "DHCP has DNS server" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65 +msgid "Diagnostics" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79 +msgid "Disable autostart" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:239 +msgid "Disable QUIC" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:240 +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:302 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:382 +msgid "Disabled" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:14 +msgid "DNS checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:64 +msgid "DNS checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:84 +msgid "DNS on router" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:179 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15 +msgid "DNS over HTTPS (DoH)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:180 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16 +msgid "DNS over TLS (DoT)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:176 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12 +msgid "DNS Protocol Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68 +msgid "DNS Rewrite TTL" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:189 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24 +msgid "DNS Server" +msgstr "" + +#: src/validators/validateDns.ts:7 +msgid "DNS server address cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:166 +msgid "Domain Resolver" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:300 +msgid "Dont Touch My DHCP!" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:238 +#: src/podkop/tabs/dashboard/initController.ts:272 +msgid "Downlink" +msgstr "" + +#: src/partials/modal/renderModal.ts:15 +msgid "Download" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:262 +msgid "Download Lists via Proxy/VPN" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:271 +msgid "Download Lists via specific proxy section" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:263 +msgid "Downloading all lists via main Proxy/VPN" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:272 +msgid "Downloading all lists via specific Proxy/VPN" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:303 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:383 +msgid "Dynamic List" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89 +msgid "Enable autostart" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:167 +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:575 +msgid "Enable Mixed Proxy" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126 +msgid "Enable Output Network Interface" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:576 +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:230 +msgid "Enable YACD" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65 +msgid "Enter complete outbound configuration in JSON format" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:338 +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:312 +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392 +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:358 +msgid "Exclude NTP" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:359 +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "" + +#: src/helpers/copyToClipboard.ts:12 +msgid "Failed to copy!" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:29 +msgid "FakeIP checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57 +msgid "FakeIP checks failed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51 +msgid "FakeIP checks partially passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:44 +msgid "FakeIP checks passed" +msgstr "" + +#: src/podkop/methods/custom/getDashboardSections.ts:117 +msgid "Fastest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:550 +msgid "Fully Routed IPs" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98 +msgid "Get global check" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:218 +msgid "Global check" +msgstr "" + +#: src/podkop/api.ts:27 +msgid "HTTP error" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:182 +msgid "Interface Monitoring" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:214 +msgid "Interface Monitoring Delay" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:183 +msgid "Interface monitoring for Bad WAN" +msgstr "" + +#: src/validators/validateDns.ts:20 +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "" + +#: src/validators/validateDomain.ts:18 +#: src/validators/validateDomain.ts:27 +msgid "Invalid domain address" +msgstr "" + +#: src/validators/validateSubnet.ts:11 +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "" + +#: src/validators/validateIp.ts:11 +msgid "Invalid IP address" +msgstr "" + +#: src/validators/validateOutboundJson.ts:19 +msgid "Invalid JSON format" +msgstr "" + +#: src/validators/validatePath.ts:22 +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:85 +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:37 +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:27 +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:46 +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:76 +msgid "Invalid Shadowsocks URL: missing port" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:67 +msgid "Invalid Shadowsocks URL: missing server" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:58 +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:16 +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:8 +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:91 +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "" + +#: src/validators/validateSocksUrl.ts:73 +msgid "Invalid SOCKS URL: invalid host format" +msgstr "" + +#: src/validators/validateSocksUrl.ts:63 +msgid "Invalid SOCKS URL: invalid port number" +msgstr "" + +#: src/validators/validateSocksUrl.ts:42 +msgid "Invalid SOCKS URL: missing host and port" +msgstr "" + +#: src/validators/validateSocksUrl.ts:51 +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "" + +#: src/validators/validateSocksUrl.ts:56 +msgid "Invalid SOCKS URL: missing port" +msgstr "" + +#: src/validators/validateSocksUrl.ts:34 +msgid "Invalid SOCKS URL: missing username" +msgstr "" + +#: src/validators/validateSocksUrl.ts:19 +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "" + +#: src/validators/validateSocksUrl.ts:10 +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "" + +#: src/validators/validateSocksUrl.ts:77 +msgid "Invalid SOCKS URL: parsing failed" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:15 +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:8 +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:56 +msgid "Invalid Trojan URL: parsing failed" +msgstr "" + +#: src/validators/validateUrl.ts:18 +msgid "Invalid URL format" +msgstr "" + +#: src/validators/validateVlessUrl.ts:109 +msgid "Invalid VLESS URL: parsing failed" +msgstr "" + +#: src/validators/validateSubnet.ts:18 +msgid "IP address 0.0.0.0 is not allowed" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:404 +msgid "Latest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:250 +msgid "List Update Frequency" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458 +msgid "Local Domain Lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:481 +msgid "Local Subnet Lists" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:79 +msgid "Main DNS" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:308 +msgid "Memory Usage" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:586 +msgid "Mixed Proxy Port" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:191 +msgid "Monitored Interfaces" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:120 +msgid "Network Interface" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:24 +msgid "Nftables checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:75 +msgid "Nftables checks partially passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74 +msgid "Nftables checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:116 +msgid "No other marking rules found" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189 +msgid "Not implement yet" +msgstr "" + +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:55 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:63 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:71 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:79 +msgid "Not running" +msgstr "" + +#: src/helpers/withTimeout.ts:7 +msgid "Operation timed out" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26 +msgid "Outbound Config" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:64 +msgid "Outbound Configuration" +msgstr "" + +#: src/validators/validateOutboundJson.ts:11 +msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:394 +msgid "Outdated" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135 +msgid "Output Network Interface" +msgstr "" + +#: src/validators/validatePath.ts:7 +msgid "Path cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:340 +msgid "Path must be absolute (start with /)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349 +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:344 +msgid "Path must end with cache.db" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:340 +msgid "Podkop" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26 +msgid "Podkop Settings" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:301 +msgid "Podkop will not modify your DHCP configuration" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:34 +msgid "Proxy Configuration URL" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:89 +msgid "Proxy traffic is not routed via FakeIP" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:88 +msgid "Proxy traffic is routed via FakeIP" +msgstr "" + +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:95 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:103 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:111 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:119 +msgid "Queued" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:245 +msgid "Regional options cannot be used together" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:504 +msgid "Remote Domain Lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:527 +msgid "Remote Subnet Lists" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49 +msgid "Restart podkop" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:74 +msgid "Router DNS is not routed through sing-box" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:73 +msgid "Router DNS is routed through sing-box" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:369 +msgid "Routing Excluded IPs" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:90 +msgid "Rules mangle counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:85 +msgid "Rules mangle exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:100 +msgid "Rules mangle output counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:95 +msgid "Rules mangle output exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:110 +msgid "Rules proxy counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105 +msgid "Rules proxy exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15 +msgid "Run Diagnostic" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264 +msgid "Russia inside restrictions" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36 +msgid "Sections" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:212 +msgid "Select a predefined list for routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13 +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13 +msgid "Select DNS protocol to use" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:251 +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23 +msgid "Select how to configure the proxy" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:121 +msgid "Select network interface for VPN connection" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:190 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25 +msgid "Select or enter DNS server address" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:323 +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:310 +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:177 +msgid "Select the DNS protocol type for the domain resolver" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300 +msgid "Select the list type for adding custom domains" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:380 +msgid "Select the list type for adding custom subnets" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90 +msgid "Select the network interface from which the traffic will originate" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136 +msgid "Select the network interface to which the traffic will originate" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:192 +msgid "Select the WAN interfaces to be monitored" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:337 +msgid "Services info" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49 +msgid "Settings" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:278 +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116 +msgid "Show sing-box config" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:351 +msgid "Sing-box" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:86 +msgid "Sing-box autostart disabled" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:19 +msgid "Sing-box checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:66 +msgid "Sing-box checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:71 +msgid "Sing-box installed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:96 +msgid "Sing-box listening ports" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:91 +msgid "Sing-box process running" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:81 +msgid "Sing-box service exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:76 +msgid "Sing-box version >= 1.12.4" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89 +msgid "Source Network Interface" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370 +msgid "Specify a local IP address to be excluded from routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:551 +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505 +msgid "Specify remote URLs to download and use domain lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:528 +msgid "Specify remote URLs to download and use subnet lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:459 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:482 +msgid "Specify the path to the list file located on the router filesystem" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69 +msgid "Start podkop" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59 +msgid "Stop podkop" +msgstr "" + +#: src/helpers/copyToClipboard.ts:10 +msgid "Successfully copied!" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:301 +msgid "System info" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:80 +msgid "Table exist" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderSections.ts:108 +msgid "Test latency" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:304 +msgid "Text List" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:384 +msgid "Text List (comma/space/newline separated)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69 +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:235 +msgid "Traffic" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:265 +msgid "Traffic Total" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80 +msgid "TTL must be a positive number" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75 +msgid "TTL value cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:181 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17 +msgid "UDP (Unprotected DNS)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:110 +msgid "UDP over TCP" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:34 +#: src/podkop/tabs/diagnostic/initController.ts:35 +#: src/podkop/tabs/diagnostic/initController.ts:36 +#: src/podkop/tabs/diagnostic/initController.ts:37 +#: src/podkop/tabs/diagnostic/initController.ts:38 +#: src/podkop/tabs/diagnostic/initController.ts:39 +#: src/podkop/tabs/diagnostic/initController.ts:373 +msgid "unknown" +msgstr "" + +#: src/podkop/api.ts:40 +msgid "Unknown error" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:237 +#: src/podkop/tabs/dashboard/initController.ts:268 +msgid "Uplink" +msgstr "" + +#: src/validators/validateProxyUrl.ts:27 +msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +msgstr "" + +#: src/validators/validateUrl.ts:13 +msgid "URL must use one of the following protocols:" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27 +msgid "URLTest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:87 +msgid "URLTest Proxy Links" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299 +msgid "User Domain List Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:311 +msgid "User Domains" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:337 +msgid "User Domains List" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:379 +msgid "User Subnet List Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391 +msgid "User Subnets" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:417 +msgid "User Subnets List" +msgstr "" + +#: src/validators/validateDns.ts:11 +#: src/validators/validateDns.ts:15 +#: src/validators/validateDomain.ts:13 +#: src/validators/validateDomain.ts:30 +#: src/validators/validateIp.ts:8 +#: src/validators/validateOutboundJson.ts:17 +#: src/validators/validatePath.ts:16 +#: src/validators/validateShadowsocksUrl.ts:95 +#: src/validators/validateSocksUrl.ts:80 +#: src/validators/validateSubnet.ts:38 +#: src/validators/validateTrojanUrl.ts:59 +#: src/validators/validateUrl.ts:16 +#: src/validators/validateVlessUrl.ts:107 +msgid "Valid" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:370 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:449 +msgid "Validation errors:" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:248 +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107 +msgid "View logs" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:247 +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:266 +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127 +msgid "You can select Output Network Interface, by default autodetect" +msgstr "" diff --git a/fe-app-podkop/locales/podkop.ru.po b/fe-app-podkop/locales/podkop.ru.po new file mode 100644 index 0000000..1754091 --- /dev/null +++ b/fe-app-podkop/locales/podkop.ru.po @@ -0,0 +1,714 @@ +# RU translations for PODKOP package. +# Copyright (C) 2025 THE PODKOP'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PODKOP package. +# divocat, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PODKOP\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-21 23:02+0300\n" +"PO-Revision-Date: 2025-10-21 23:02+0300\n" +"Last-Translator: divocat\n" +"Language-Team: none\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "✔ Enabled" +msgstr "✔ Включено" + +msgid "✔ Running" +msgstr "✔ Работает" + +msgid "✘ Disabled" +msgstr "✘ Отключено" + +msgid "✘ Stopped" +msgstr "✘ Остановлен" + +msgid "Active Connections" +msgstr "Активные соединения" + +msgid "Additional marking rules found" +msgstr "Найдены дополнительные правила маркировки" + +msgid "Applicable for SOCKS and Shadowsocks proxy" +msgstr "Применимо для SOCKS и Shadowsocks прокси" + +msgid "At least one valid domain must be specified. Comments-only content is not allowed." +msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается." + +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." +msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." + +msgid "Bootsrap DNS" +msgstr "Bootstrap DNS" + +msgid "Bootstrap DNS server" +msgstr "Bootstrap DNS-сервер" + +msgid "Browser is not using FakeIP" +msgstr "Браузер не использует FakeIP" + +msgid "Browser is using FakeIP correctly" +msgstr "Браузер использует FakeIP" + +msgid "Cache File Path" +msgstr "Путь к файлу кэша" + +msgid "Cache file path cannot be empty" +msgstr "Путь к файлу кэша не может быть пустым" + +msgid "Cannot receive DNS checks result" +msgstr "Не удалось получить результаты проверки DNS" + +msgid "Cannot receive nftables checks result" +msgstr "Не удалось получить результаты проверки nftables" + +msgid "Cannot receive Sing-box checks result" +msgstr "Не удалось получить результаты проверки Sing-box" + +msgid "Checking dns, please wait" +msgstr "Проверка dns, пожалуйста подождите" + +msgid "Checking FakeIP, please wait" +msgstr "Проверка FakeIP, пожалуйста подождите" + +msgid "Checking nftables, please wait" +msgstr "Проверка nftables, пожалуйста подождите" + +msgid "Checking sing-box, please wait" +msgstr "Проверка sing-box, пожалуйста подождите" + +msgid "CIDR must be between 0 and 32" +msgstr "CIDR должен быть между 0 и 32" + +msgid "Close" +msgstr "Закрыть" + +msgid "Community Lists" +msgstr "Списки сообщества" + +msgid "Config File Path" +msgstr "Путь к файлу конфигурации" + +msgid "Configuration for Podkop service" +msgstr "Настройки сервиса Podkop" + +msgid "Configuration Type" +msgstr "Тип конфигурации" + +msgid "Connection Type" +msgstr "Тип подключения" + +msgid "Connection URL" +msgstr "URL подключения" + +msgid "Copy" +msgstr "Копировать" + +msgid "Currently unavailable" +msgstr "Временно недоступно" + +msgid "Dashboard" +msgstr "Дашборд" + +msgid "Dashboard currently unavailable" +msgstr "Дашборд сейчас недоступен" + +msgid "Delay in milliseconds before reloading podkop after interface UP" +msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" + +msgid "Delay value cannot be empty" +msgstr "Значение задержки не может быть пустым" + +msgid "DHCP has DNS server" +msgstr "DHCP содержит DNS сервер" + +msgid "Diagnostics" +msgstr "Диагностика" + +msgid "Disable autostart" +msgstr "Отключить автостарт" + +msgid "Disable QUIC" +msgstr "Отключить QUIC" + +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "Отключить QUIC протокол для улучшения совместимости или исправления видео стриминга" + +msgid "Disabled" +msgstr "Отключено" + +msgid "DNS checks" +msgstr "DNS проверки" + +msgid "DNS checks passed" +msgstr "DNS проверки успешно завершены" + +msgid "DNS on router" +msgstr "DNS на роутере" + +msgid "DNS over HTTPS (DoH)" +msgstr "DNS через HTTPS (DoH)" + +msgid "DNS over TLS (DoT)" +msgstr "DNS через TLS (DoT)" + +msgid "DNS Protocol Type" +msgstr "Тип протокола DNS" + +msgid "DNS Rewrite TTL" +msgstr "Перезапись TTL для DNS" + +msgid "DNS Server" +msgstr "DNS-сервер" + +msgid "DNS server address cannot be empty" +msgstr "Адрес DNS-сервера не может быть пустым" + +msgid "Domain Resolver" +msgstr "Резолвер доменов" + +msgid "Dont Touch My DHCP!" +msgstr "Dont Touch My DHCP!" + +msgid "Downlink" +msgstr "Входящий" + +msgid "Download" +msgstr "Скачать" + +msgid "Download Lists via Proxy/VPN" +msgstr "Скачивать списки через Proxy/VPN" + +msgid "Download Lists via specific proxy section" +msgstr "Скачивать списки через выбранную секцию" + +msgid "Downloading all lists via main Proxy/VPN" +msgstr "Загрузка всех списков через основной прокси/VPN" + +msgid "Downloading all lists via specific Proxy/VPN" +msgstr "Загрузка всех списков через указанный прокси/VPN" + +msgid "Dynamic List" +msgstr "Динамический список" + +msgid "Enable autostart" +msgstr "Включить автостарт" + +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе" + +msgid "Enable Mixed Proxy" +msgstr "Включить смешанный прокси" + +msgid "Enable Output Network Interface" +msgstr "Включить выходной сетевой интерфейс" + +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "Включить смешанный прокси-сервер, разрешив этому разделу маршрутизировать трафик как через HTTP, так и через SOCKS-прокси." + +msgid "Enable YACD" +msgstr "Включить YACD" + +msgid "Enter complete outbound configuration in JSON format" +msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" + +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "Введите доменные имена, разделяя их запятыми, пробелами или переносами строк. Вы можете добавлять комментарии, используя //" + +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "Введите доменные имена без протоколов, например example.com или sub.example.com" + +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" + +msgid "Exclude NTP" +msgstr "Исключить NTP" + +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN." + +msgid "Failed to copy!" +msgstr "Не удалось скопировать!" + +msgid "FakeIP checks" +msgstr "Проверка FakeIP" + +msgid "FakeIP checks failed" +msgstr "Проверки FakeIP не пройдены" + +msgid "FakeIP checks partially passed" +msgstr "Проверка FakeIP частично пройдена" + +msgid "FakeIP checks passed" +msgstr "Проверки FakeIP пройдены" + +msgid "Fastest" +msgstr "Самый быстрый" + +msgid "Fully Routed IPs" +msgstr "Полностью маршрутизированные IP-адреса" + +msgid "Get global check" +msgstr "Получить глобальную проверку" + +msgid "Global check" +msgstr "Глобальная проверка" + +msgid "HTTP error" +msgstr "Ошибка HTTP" + +msgid "Interface Monitoring" +msgstr "Мониторинг интерфейса" + +msgid "Interface Monitoring Delay" +msgstr "Задержка при мониторинге интерфейсов" + +msgid "Interface monitoring for Bad WAN" +msgstr "Мониторинг интерфейса для Bad WAN" + +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH" + +msgid "Invalid domain address" +msgstr "Неверный домен" + +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" + +msgid "Invalid IP address" +msgstr "Неверный IP-адрес" + +msgid "Invalid JSON format" +msgstr "Неверный формат JSON" + +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы" + +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "Неверный номер порта. Допустимо от 1 до 65535" + +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password" + +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные" + +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" + +msgid "Invalid Shadowsocks URL: missing port" +msgstr "Неверный URL Shadowsocks: отсутствует порт" + +msgid "Invalid Shadowsocks URL: missing server" +msgstr "Неверный URL Shadowsocks: отсутствует сервер" + +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера" + +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "Неверный URL Shadowsocks: не должен содержать пробелов" + +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "Неверный URL Shadowsocks: должен начинаться с ss://" + +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "Неверный URL Shadowsocks: ошибка разбора" + +msgid "Invalid SOCKS URL: invalid host format" +msgstr "Неверный URL SOCKS: неверный формат хоста" + +msgid "Invalid SOCKS URL: invalid port number" +msgstr "Неверный URL SOCKS: неверный номер порта" + +msgid "Invalid SOCKS URL: missing host and port" +msgstr "Неверный URL SOCKS: отсутствует хост и порт" + +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "Неверный URL SOCKS: отсутствует имя хоста или IP-адрес" + +msgid "Invalid SOCKS URL: missing port" +msgstr "Неверный URL SOCKS: отсутствует порт" + +msgid "Invalid SOCKS URL: missing username" +msgstr "Неверный URL SOCKS: отсутствует имя пользователя" + +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "Неверный URL SOCKS: не должен содержать пробелов" + +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "Неверный URL-адрес SOCKS: должен начинаться с socks4://, socks4a:// или socks5://" + +msgid "Invalid SOCKS URL: parsing failed" +msgstr "Неверный URL SOCKS: парсинг не удался" + +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "Неверный URL Trojan: не должен содержать пробелов" + +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "Неверный URL Trojan: должен начинаться с trojan://" + +msgid "Invalid Trojan URL: parsing failed" +msgstr "Неверный URL Trojan: ошибка разбора" + +msgid "Invalid URL format" +msgstr "Неверный формат URL" + +msgid "Invalid VLESS URL: parsing failed" +msgstr "Неверный URL VLESS: ошибка разбора" + +msgid "IP address 0.0.0.0 is not allowed" +msgstr "IP-адрес 0.0.0.0 не допускается" + +msgid "Latest" +msgstr "Последняя" + +msgid "List Update Frequency" +msgstr "Частота обновления списков" + +msgid "Local Domain Lists" +msgstr "Локальные списки доменов" + +msgid "Local Subnet Lists" +msgstr "Локальные списки подсетей" + +msgid "Main DNS" +msgstr "Основной DNS" + +msgid "Memory Usage" +msgstr "Использование памяти" + +msgid "Mixed Proxy Port" +msgstr "Порт смешанного прокси" + +msgid "Monitored Interfaces" +msgstr "Наблюдаемые интерфейсы" + +msgid "Network Interface" +msgstr "Сетевой интерфейс" + +msgid "Nftables checks" +msgstr "Проверки Nftables" + +msgid "Nftables checks partially passed" +msgstr "Проверки Nftables частично пройдена" + +msgid "Nftables checks passed" +msgstr "Nftables проверки успешно завершены" + +msgid "No other marking rules found" +msgstr "Другие правила маркировки не найдены" + +msgid "Not implement yet" +msgstr "Ещё не реализовано" + +msgid "Not running" +msgstr "Не запущено" + +msgid "Operation timed out" +msgstr "Время ожидания истекло" + +msgid "Outbound Config" +msgstr "Конфигурация Outbound" + +msgid "Outbound Configuration" +msgstr "Конфигурация исходящего соединения" + +msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" +msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\"" + +msgid "Outdated" +msgstr "Устаревшая" + +msgid "Output Network Interface" +msgstr "Выходной сетевой интерфейс" + +msgid "Path cannot be empty" +msgstr "Путь не может быть пустым" + +msgid "Path must be absolute (start with /)" +msgstr "Путь должен быть абсолютным (начинаться с /)" + +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" + +msgid "Path must end with cache.db" +msgstr "Путь должен заканчиваться на cache.db" + +msgid "Podkop" +msgstr "Podkop" + +msgid "Podkop Settings" +msgstr "Настройки podkop" + +msgid "Podkop will not modify your DHCP configuration" +msgstr "Podkop не будет изменять вашу конфигурацию DHCP." + +msgid "Proxy Configuration URL" +msgstr "URL конфигурации прокси" + +msgid "Proxy traffic is not routed via FakeIP" +msgstr "Прокси-трафик не маршрутизируется через FakeIP" + +msgid "Proxy traffic is routed via FakeIP" +msgstr "Прокси-трафик направляется через FakeIP" + +msgid "Queued" +msgstr "В очереди" + +msgid "Regional options cannot be used together" +msgstr "Нельзя использовать несколько региональных опций одновременно" + +msgid "Remote Domain Lists" +msgstr "Удалённые списки доменов" + +msgid "Remote Subnet Lists" +msgstr "Удалённые списки подсетей" + +msgid "Restart podkop" +msgstr "Перезапустить Podkop" + +msgid "Router DNS is not routed through sing-box" +msgstr "DNS роутера не проходит через sing-box" + +msgid "Router DNS is routed through sing-box" +msgstr "DNS роутера проходит через sing-box" + +msgid "Routing Excluded IPs" +msgstr "Исключённые из маршрутизации IP-адреса" + +msgid "Rules mangle counters" +msgstr "Счётчики правил mangle" + +msgid "Rules mangle exist" +msgstr "Правила mangle существуют" + +msgid "Rules mangle output counters" +msgstr "Счётчики правил mangle output" + +msgid "Rules mangle output exist" +msgstr "Правила mangle output существуют" + +msgid "Rules proxy counters" +msgstr "Счётчики правил proxy" + +msgid "Rules proxy exist" +msgstr "Правила прокси существуют" + +msgid "Run Diagnostic" +msgstr "Запустить диагностику" + +msgid "Russia inside restrictions" +msgstr "Ограничения Russia inside" + +msgid "Sections" +msgstr "Секции" + +msgid "Select a predefined list for routing" +msgstr "Выберите предопределенный список для маршрутизации" + +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика" + +msgid "Select DNS protocol to use" +msgstr "Выберите протокол DNS" + +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "Выберите частоту автоматического обновления списков доменов или подсетей." + +msgid "Select how to configure the proxy" +msgstr "Выберите способ настройки прокси" + +msgid "Select network interface for VPN connection" +msgstr "Выберите сетевой интерфейс для VPN подключения" + +msgid "Select or enter DNS server address" +msgstr "Выберите или введите адрес DNS-сервера" + +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Select the DNS protocol type for the domain resolver" +msgstr "Выберите тип протокола DNS для резолвера доменов" + +msgid "Select the list type for adding custom domains" +msgstr "Выберите тип списка для добавления пользовательских доменов" + +msgid "Select the list type for adding custom subnets" +msgstr "Выберите тип списка для добавления пользовательских подсетей" + +msgid "Select the network interface from which the traffic will originate" +msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" + +msgid "Select the network interface to which the traffic will originate" +msgstr "Выберите сетевой интерфейс, на который будет поступать трафик." + +msgid "Select the WAN interfaces to be monitored" +msgstr "Выберите WAN интерфейсы для мониторинга" + +msgid "Services info" +msgstr "Информация о сервисах" + +msgid "Settings" +msgstr "Настройки" + +msgid "Show sing-box config" +msgstr "Показать sing-box конфигурацию" + +msgid "Sing-box" +msgstr "Sing-box" + +msgid "Sing-box autostart disabled" +msgstr "Автостарт sing-box отключен" + +msgid "Sing-box checks" +msgstr "Sing-box проверки" + +msgid "Sing-box checks passed" +msgstr "Sing-box проверки успешно завершены" + +msgid "Sing-box installed" +msgstr "Sing-box установлен" + +msgid "Sing-box listening ports" +msgstr "Sing-box слушает порты" + +msgid "Sing-box process running" +msgstr "Процесс sing-box запущен" + +msgid "Sing-box service exist" +msgstr "Сервис sing-box существует" + +msgid "Sing-box version >= 1.12.4" +msgstr "Версия sing-box >= 1.12.4" + +msgid "Source Network Interface" +msgstr "Сетевой интерфейс источника" + +msgid "Specify a local IP address to be excluded from routing" +msgstr "Укажите локальный IP-адрес, который следует исключить из маршрутизации." + +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут." + +msgid "Specify remote URLs to download and use domain lists" +msgstr "Укажите удаленные URL-адреса для загрузки и использования списков доменов." + +msgid "Specify remote URLs to download and use subnet lists" +msgstr "Укажите удаленные URL-адреса для загрузки и использования списков подсетей." + +msgid "Specify the path to the list file located on the router filesystem" +msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." + +msgid "Start podkop" +msgstr "Запустить podkop" + +msgid "Stop podkop" +msgstr "Остановить podkop" + +msgid "Successfully copied!" +msgstr "Успешно скопировано!" + +msgid "System info" +msgstr "Системная информация" + +msgid "Table exist" +msgstr "Таблица существует" + +msgid "Test latency" +msgstr "Измерить задержки" + +msgid "Text List" +msgstr "Текстовый список" + +msgid "Text List (comma/space/newline separated)" +msgstr "Текстовый список (через запятую, пробел или новую строку)" + +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" + +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" + +msgid "Traffic" +msgstr "Трафик" + +msgid "Traffic Total" +msgstr "Всего трафика" + +msgid "TTL must be a positive number" +msgstr "TTL должно быть положительным числом" + +msgid "TTL value cannot be empty" +msgstr "Значение TTL не может быть пустым" + +msgid "UDP (Unprotected DNS)" +msgstr "UDP (Незащищённый DNS)" + +msgid "UDP over TCP" +msgstr "UDP через TCP" + +msgid "unknown" +msgstr "неизвестно" + +msgid "Unknown error" +msgstr "Неизвестная ошибка" + +msgid "Uplink" +msgstr "Исходящий" + +msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://" + +msgid "URL must use one of the following protocols:" +msgstr "URL должен использовать один из следующих протоколов:" + +msgid "URLTest" +msgstr "URLTest" + +msgid "URLTest Proxy Links" +msgstr "Ссылки прокси для URLTest" + +msgid "User Domain List Type" +msgstr "Тип пользовательского списка доменов" + +msgid "User Domains" +msgstr "Пользовательские домены" + +msgid "User Domains List" +msgstr "Список пользовательских доменов" + +msgid "User Subnet List Type" +msgstr "Тип пользовательского списка подсетей" + +msgid "User Subnets" +msgstr "Пользовательские подсети" + +msgid "User Subnets List" +msgstr "Список пользовательских подсетей" + +msgid "Valid" +msgstr "Валидно" + +msgid "Validation errors:" +msgstr "Ошибки валидации:" + +msgid "View logs" +msgstr "Посмотреть логи" + +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." + +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." + +msgid "You can select Output Network Interface, by default autodetect" +msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." diff --git a/fe-app-podkop/package.json b/fe-app-podkop/package.json index 6241ec2..ac1c001 100644 --- a/fe-app-podkop/package.json +++ b/fe-app-podkop/package.json @@ -5,21 +5,30 @@ "type": "module", "scripts": { "format": "prettier --write src", + "format:js": "prettier --write ../luci-app-podkop/htdocs/luci-static/resources/view/podkop", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", "build": "tsup src/main.ts", "dev": "tsup src/main.ts --watch", "test": "vitest", "ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build", - "watch:sftp": "node watch-upload.js" + "watch:sftp": "node watch-upload.js", + "locales:exctract-calls": "node extract-calls.js", + "locales:generate-pot": "node generate-pot.js", + "locales:generate-po:ru": "node generate-po.js ru", + "locales:distribute": "node distribute-locales.js", + "locales:actualize": "yarn locales:exctract-calls && yarn locales:generate-pot && yarn locales:generate-po:ru && yarn locales:distribute" }, "devDependencies": { + "@babel/parser": "7.28.4", + "@babel/traverse": "7.28.4", "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "chokidar": "4.0.3", "dotenv": "17.2.3", "eslint": "9.36.0", "eslint-config-prettier": "10.1.8", + "fast-glob": "3.3.3", "glob": "11.0.3", "prettier": "3.6.2", "ssh2-sftp-client": "12.0.1", diff --git a/fe-app-podkop/src/clash/index.ts b/fe-app-podkop/src/clash/index.ts deleted file mode 100644 index c3b7574..0000000 --- a/fe-app-podkop/src/clash/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './methods'; diff --git a/fe-app-podkop/src/clash/methods/createBaseApiRequest.ts b/fe-app-podkop/src/clash/methods/createBaseApiRequest.ts deleted file mode 100644 index 601a433..0000000 --- a/fe-app-podkop/src/clash/methods/createBaseApiRequest.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IBaseApiResponse } from '../types'; - -export async function createBaseApiRequest( - fetchFn: () => Promise, -): Promise> { - try { - const response = await fetchFn(); - - if (!response.ok) { - return { - success: false as const, - message: `${_('HTTP error')} ${response.status}: ${response.statusText}`, - }; - } - - const data: T = await response.json(); - - return { - success: true as const, - data, - }; - } catch (e) { - return { - success: false as const, - message: e instanceof Error ? e.message : _('Unknown error'), - }; - } -} diff --git a/fe-app-podkop/src/clash/methods/getConfig.ts b/fe-app-podkop/src/clash/methods/getConfig.ts deleted file mode 100644 index 8f7135a..0000000 --- a/fe-app-podkop/src/clash/methods/getConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ClashAPI, IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function getClashConfig(): Promise< - IBaseApiResponse -> { - return createBaseApiRequest(() => - fetch(`${getClashApiUrl()}/configs`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); -} diff --git a/fe-app-podkop/src/clash/methods/getGroupDelay.ts b/fe-app-podkop/src/clash/methods/getGroupDelay.ts deleted file mode 100644 index f160bec..0000000 --- a/fe-app-podkop/src/clash/methods/getGroupDelay.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ClashAPI, IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function getClashGroupDelay( - group: string, - url = 'https://www.gstatic.com/generate_204', - timeout = 2000, -): Promise> { - const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent( - url, - )}&timeout=${timeout}`; - - return createBaseApiRequest(() => - fetch(endpoint, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); -} diff --git a/fe-app-podkop/src/clash/methods/getProxies.ts b/fe-app-podkop/src/clash/methods/getProxies.ts deleted file mode 100644 index e465c58..0000000 --- a/fe-app-podkop/src/clash/methods/getProxies.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ClashAPI, IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function getClashProxies(): Promise< - IBaseApiResponse -> { - return createBaseApiRequest(() => - fetch(`${getClashApiUrl()}/proxies`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); -} diff --git a/fe-app-podkop/src/clash/methods/getVersion.ts b/fe-app-podkop/src/clash/methods/getVersion.ts deleted file mode 100644 index 119db9f..0000000 --- a/fe-app-podkop/src/clash/methods/getVersion.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ClashAPI, IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function getClashVersion(): Promise< - IBaseApiResponse -> { - return createBaseApiRequest(() => - fetch(`${getClashApiUrl()}/version`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); -} diff --git a/fe-app-podkop/src/clash/methods/index.ts b/fe-app-podkop/src/clash/methods/index.ts deleted file mode 100644 index 1feccdb..0000000 --- a/fe-app-podkop/src/clash/methods/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './createBaseApiRequest'; -export * from './getConfig'; -export * from './getGroupDelay'; -export * from './getProxies'; -export * from './getVersion'; -export * from './triggerProxySelector'; -export * from './triggerLatencyTest'; diff --git a/fe-app-podkop/src/clash/methods/triggerLatencyTest.ts b/fe-app-podkop/src/clash/methods/triggerLatencyTest.ts deleted file mode 100644 index b7fffd9..0000000 --- a/fe-app-podkop/src/clash/methods/triggerLatencyTest.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function triggerLatencyGroupTest( - tag: string, - timeout: number = 5000, - url: string = 'https://www.gstatic.com/generate_204', -): Promise> { - return createBaseApiRequest(() => - fetch( - `${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`, - { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }, - ), - ); -} - -export async function triggerLatencyProxyTest( - tag: string, - timeout: number = 2000, - url: string = 'https://www.gstatic.com/generate_204', -): Promise> { - return createBaseApiRequest(() => - fetch( - `${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`, - { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }, - ), - ); -} diff --git a/fe-app-podkop/src/clash/methods/triggerProxySelector.ts b/fe-app-podkop/src/clash/methods/triggerProxySelector.ts deleted file mode 100644 index 16d1f55..0000000 --- a/fe-app-podkop/src/clash/methods/triggerProxySelector.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IBaseApiResponse } from '../types'; -import { createBaseApiRequest } from './createBaseApiRequest'; -import { getClashApiUrl } from '../../helpers'; - -export async function triggerProxySelector( - selector: string, - outbound: string, -): Promise> { - return createBaseApiRequest(() => - fetch(`${getClashApiUrl()}/proxies/${selector}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: outbound }), - }), - ); -} diff --git a/fe-app-podkop/src/clash/types.ts b/fe-app-podkop/src/clash/types.ts deleted file mode 100644 index a54a55f..0000000 --- a/fe-app-podkop/src/clash/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type IBaseApiResponse = - | { - success: true; - data: T; - } - | { - success: false; - message: string; - }; - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace ClashAPI { - export interface Version { - meta: boolean; - premium: boolean; - version: string; - } - - export interface Config { - port: number; - 'socks-port': number; - 'redir-port': number; - 'tproxy-port': number; - 'mixed-port': number; - 'allow-lan': boolean; - 'bind-address': string; - mode: 'Rule' | 'Global' | 'Direct'; - 'mode-list': string[]; - 'log-level': 'debug' | 'info' | 'warn' | 'error'; - ipv6: boolean; - tun: null | Record; - } - - export interface ProxyHistoryEntry { - time: string; - delay: number; - } - - export interface ProxyBase { - type: string; - name: string; - udp: boolean; - history: ProxyHistoryEntry[]; - now?: string; - all?: string[]; - } - - export interface Proxies { - proxies: Record; - } - - export type Delays = Record; -} diff --git a/fe-app-podkop/src/helpers/copyToClipboard.ts b/fe-app-podkop/src/helpers/copyToClipboard.ts new file mode 100644 index 0000000..c9e97a8 --- /dev/null +++ b/fe-app-podkop/src/helpers/copyToClipboard.ts @@ -0,0 +1,16 @@ +import { showToast } from './showToast'; + +export function copyToClipboard(text: string) { + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + showToast(_('Successfully copied!'), 'success'); + } catch (_err) { + showToast(_('Failed to copy!'), 'error'); + console.error('copyToClipboard - e', _err); + } + document.body.removeChild(textarea); +} diff --git a/fe-app-podkop/src/helpers/downloadAsTxt.ts b/fe-app-podkop/src/helpers/downloadAsTxt.ts new file mode 100644 index 0000000..a0d9222 --- /dev/null +++ b/fe-app-podkop/src/helpers/downloadAsTxt.ts @@ -0,0 +1,15 @@ +export function downloadAsTxt(text: string, filename: string) { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + + const safeName = filename.endsWith('.txt') ? filename : `${filename}.txt`; + link.download = safeName; + + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(link.href); +} diff --git a/fe-app-podkop/src/helpers/getBaseUrl.ts b/fe-app-podkop/src/helpers/getBaseUrl.ts deleted file mode 100644 index 88b82ff..0000000 --- a/fe-app-podkop/src/helpers/getBaseUrl.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getBaseUrl(): string { - const { protocol, hostname } = window.location; - return `${protocol}//${hostname}`; -} diff --git a/fe-app-podkop/src/helpers/getClashApiUrl.ts b/fe-app-podkop/src/helpers/getClashApiUrl.ts index 27032a0..0428f3d 100644 --- a/fe-app-podkop/src/helpers/getClashApiUrl.ts +++ b/fe-app-podkop/src/helpers/getClashApiUrl.ts @@ -1,9 +1,3 @@ -export function getClashApiUrl(): string { - const { hostname } = window.location; - - return `http://${hostname}:9090`; -} - export function getClashWsUrl(): string { const { hostname } = window.location; diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index 9b48e5b..06baeb7 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -1,4 +1,3 @@ -export * from './getBaseUrl'; export * from './parseValueList'; export * from './injectGlobalStyles'; export * from './withTimeout'; @@ -10,3 +9,5 @@ export * from './getClashApiUrl'; export * from './splitProxyString'; export * from './preserveScrollForPage'; export * from './parseQueryString'; +export * from './svgEl'; +export * from './insertIf'; diff --git a/fe-app-podkop/src/helpers/insertIf.ts b/fe-app-podkop/src/helpers/insertIf.ts new file mode 100644 index 0000000..cfb93f3 --- /dev/null +++ b/fe-app-podkop/src/helpers/insertIf.ts @@ -0,0 +1,7 @@ +export function insertIf(condition: boolean, elements: Array) { + return condition ? elements : ([] as Array); +} + +export function insertIfObj(condition: boolean, object: T) { + return condition ? object : ({} as T); +} diff --git a/fe-app-podkop/src/helpers/normalizeCompiledVersion.ts b/fe-app-podkop/src/helpers/normalizeCompiledVersion.ts new file mode 100644 index 0000000..60274b0 --- /dev/null +++ b/fe-app-podkop/src/helpers/normalizeCompiledVersion.ts @@ -0,0 +1,7 @@ +export function normalizeCompiledVersion(version: string) { + if (version.includes('COMPILED')) { + return 'dev'; + } + + return version; +} diff --git a/fe-app-podkop/src/helpers/showToast.ts b/fe-app-podkop/src/helpers/showToast.ts new file mode 100644 index 0000000..92dcdd9 --- /dev/null +++ b/fe-app-podkop/src/helpers/showToast.ts @@ -0,0 +1,24 @@ +export function showToast( + message: string, + type: 'success' | 'error', + duration: number = 3000, +) { + let container = document.querySelector('.toast-container'); + if (!container) { + container = document.createElement('div'); + container.className = 'toast-container'; + document.body.appendChild(container); + } + + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + + container.appendChild(toast); + setTimeout(() => toast.classList.add('visible'), 100); + + setTimeout(() => { + toast.classList.remove('visible'); + setTimeout(() => toast.remove(), 300); + }, duration); +} diff --git a/fe-app-podkop/src/helpers/svgEl.ts b/fe-app-podkop/src/helpers/svgEl.ts new file mode 100644 index 0000000..f7ebfd4 --- /dev/null +++ b/fe-app-podkop/src/helpers/svgEl.ts @@ -0,0 +1,18 @@ +export function svgEl( + tag: K, + attrs: Partial> = {}, + children: (SVGElement | null | undefined)[] = [], +): SVGElementTagNameMap[K] { + const NS = 'http://www.w3.org/2000/svg'; + const el = document.createElementNS(NS, tag); + + for (const [k, v] of Object.entries(attrs)) { + if (v != null) el.setAttribute(k, String(v)); + } + + (Array.isArray(children) ? children : [children]) + .filter(Boolean) + .forEach((ch) => el.appendChild(ch as SVGElement)); + + return el; +} diff --git a/fe-app-podkop/src/helpers/withTimeout.ts b/fe-app-podkop/src/helpers/withTimeout.ts index f06108a..e220740 100644 --- a/fe-app-podkop/src/helpers/withTimeout.ts +++ b/fe-app-podkop/src/helpers/withTimeout.ts @@ -1,3 +1,5 @@ +import { logger } from '../podkop'; + export async function withTimeout( promise: Promise, timeoutMs: number, @@ -16,6 +18,6 @@ export async function withTimeout( } finally { clearTimeout(timeoutId); const elapsed = performance.now() - start; - console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`); + logger.info('[SHELL]', `[${operationName}] took ${elapsed.toFixed(2)} ms`); } } diff --git a/fe-app-podkop/src/icons/index.ts b/fe-app-podkop/src/icons/index.ts new file mode 100644 index 0000000..71d9625 --- /dev/null +++ b/fe-app-podkop/src/icons/index.ts @@ -0,0 +1,17 @@ +export * from './renderLoaderCircleIcon24'; +export * from './renderCircleAlertIcon24'; +export * from './renderCircleCheckIcon24'; +export * from './renderCircleSlashIcon24'; +export * from './renderCircleXIcon24'; +export * from './renderCheckIcon24'; +export * from './renderXIcon24'; +export * from './renderTriangleAlertIcon24'; +export * from './renderPauseIcon24'; +export * from './renderPlayIcon24'; +export * from './renderRotateCcwIcon24'; +export * from './renderCircleStopIcon24'; +export * from './renderCirclePlayIcon24'; +export * from './renderCircleCheckBigIcon24'; +export * from './renderSquareChartGanttIcon24'; +export * from './renderCogIcon24'; +export * from './renderSearchIcon24'; diff --git a/fe-app-podkop/src/icons/renderCheckIcon24.ts b/fe-app-podkop/src/icons/renderCheckIcon24.ts new file mode 100644 index 0000000..b1c9ab2 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCheckIcon24.ts @@ -0,0 +1,23 @@ +import { svgEl } from '../helpers'; + +export function renderCheckIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-check-icon lucide-check', + }, + [ + svgEl('path', { + d: 'M20 6 9 17l-5-5', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleAlertIcon24.ts b/fe-app-podkop/src/icons/renderCircleAlertIcon24.ts new file mode 100644 index 0000000..8e6820b --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleAlertIcon24.ts @@ -0,0 +1,39 @@ +import { svgEl } from '../helpers'; + +export function renderCircleAlertIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + width: '24', + height: '24', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-alert-icon lucide-circle-alert', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('line', { + x1: '12', + y1: '8', + x2: '12', + y2: '12', + }), + svgEl('line', { + x1: '12', + y1: '16', + x2: '12.01', + y2: '16', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts b/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts new file mode 100644 index 0000000..e353091 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts @@ -0,0 +1,26 @@ +import { svgEl } from '../helpers'; + +export function renderCircleCheckBigIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-check-big-icon lucide-circle-check-big', + }, + [ + svgEl('path', { + d: 'M21.801 10A10 10 0 1 1 17 3.335', + }), + svgEl('path', { + d: 'm9 11 3 3L22 4', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleCheckIcon24.ts b/fe-app-podkop/src/icons/renderCircleCheckIcon24.ts new file mode 100644 index 0000000..4ccf018 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleCheckIcon24.ts @@ -0,0 +1,30 @@ +import { svgEl } from '../helpers'; + +export function renderCircleCheckIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + width: '24', + height: '24', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-check-icon lucide-circle-check', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('path', { + d: 'M9 12l2 2 4-4', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts b/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts new file mode 100644 index 0000000..a4102f1 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts @@ -0,0 +1,28 @@ +import { svgEl } from '../helpers'; + +export function renderCirclePlayIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-play-icon lucide-circle-play', + }, + [ + svgEl('path', { + d: 'M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z', + }), + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleSlashIcon24.ts b/fe-app-podkop/src/icons/renderCircleSlashIcon24.ts new file mode 100644 index 0000000..e082c11 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleSlashIcon24.ts @@ -0,0 +1,33 @@ +import { svgEl } from '../helpers'; + +export function renderCircleSlashIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + width: '24', + height: '24', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-slash-icon lucide-circle-slash', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('line', { + x1: '9', + y1: '15', + x2: '15', + y2: '9', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleStopIcon24.ts b/fe-app-podkop/src/icons/renderCircleStopIcon24.ts new file mode 100644 index 0000000..bb9e614 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleStopIcon24.ts @@ -0,0 +1,32 @@ +import { svgEl } from '../helpers'; + +export function renderCircleStopIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-stop-icon lucide-circle-stop', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('rect', { + x: '9', + y: '9', + width: '6', + height: '6', + rx: '1', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCircleXIcon24.ts b/fe-app-podkop/src/icons/renderCircleXIcon24.ts new file mode 100644 index 0000000..5695617 --- /dev/null +++ b/fe-app-podkop/src/icons/renderCircleXIcon24.ts @@ -0,0 +1,33 @@ +import { svgEl } from '../helpers'; + +export function renderCircleXIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + width: '24', + height: '24', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-circle-x-icon lucide-circle-x', + }, + [ + svgEl('circle', { + cx: '12', + cy: '12', + r: '10', + }), + svgEl('path', { + d: 'M15 9L9 15', + }), + svgEl('path', { + d: 'M9 9L15 15', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderCogIcon24.ts b/fe-app-podkop/src/icons/renderCogIcon24.ts new file mode 100644 index 0000000..0093bbb --- /dev/null +++ b/fe-app-podkop/src/icons/renderCogIcon24.ts @@ -0,0 +1,34 @@ +import { svgEl } from '../helpers'; + +export function renderCogIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-cog-icon lucide-cog', + }, + [ + svgEl('path', { d: 'M11 10.27 7 3.34' }), + svgEl('path', { d: 'm11 13.73-4 6.93' }), + svgEl('path', { d: 'M12 22v-2' }), + svgEl('path', { d: 'M12 2v2' }), + svgEl('path', { d: 'M14 12h8' }), + svgEl('path', { d: 'm17 20.66-1-1.73' }), + svgEl('path', { d: 'm17 3.34-1 1.73' }), + svgEl('path', { d: 'M2 12h2' }), + svgEl('path', { d: 'm20.66 17-1.73-1' }), + svgEl('path', { d: 'm20.66 7-1.73 1' }), + svgEl('path', { d: 'm3.34 17 1.73-1' }), + svgEl('path', { d: 'm3.34 7 1.73 1' }), + svgEl('circle', { cx: '12', cy: '12', r: '2' }), + svgEl('circle', { cx: '12', cy: '12', r: '8' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts b/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts new file mode 100644 index 0000000..e6ecd3d --- /dev/null +++ b/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts @@ -0,0 +1,32 @@ +import { svgEl } from '../helpers'; + +export function renderLoaderCircleIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-loader-circle rotate', + }, + [ + svgEl('path', { + d: 'M21 12a9 9 0 1 1-6.219-8.56', + }), + svgEl('animateTransform', { + attributeName: 'transform', + attributeType: 'XML', + type: 'rotate', + from: '0 12 12', + to: '360 12 12', + dur: '1s', + repeatCount: 'indefinite', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderPauseIcon24.ts b/fe-app-podkop/src/icons/renderPauseIcon24.ts new file mode 100644 index 0000000..8b150c7 --- /dev/null +++ b/fe-app-podkop/src/icons/renderPauseIcon24.ts @@ -0,0 +1,34 @@ +import { svgEl } from '../helpers'; + +export function renderPauseIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-pause-icon lucide-pause', + }, + [ + svgEl('rect', { + x: '14', + y: '3', + width: '5', + height: '18', + rx: '1', + }), + svgEl('rect', { + x: '5', + y: '3', + width: '5', + height: '18', + rx: '1', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderPlayIcon24.ts b/fe-app-podkop/src/icons/renderPlayIcon24.ts new file mode 100644 index 0000000..46c161d --- /dev/null +++ b/fe-app-podkop/src/icons/renderPlayIcon24.ts @@ -0,0 +1,23 @@ +import { svgEl } from '../helpers'; + +export function renderPlayIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-play-icon lucide-play', + }, + [ + svgEl('path', { + d: 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts b/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts new file mode 100644 index 0000000..82a5d16 --- /dev/null +++ b/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts @@ -0,0 +1,26 @@ +import { svgEl } from '../helpers'; + +export function renderRotateCcwIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-rotate-ccw-icon lucide-rotate-ccw', + }, + [ + svgEl('path', { + d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', + }), + svgEl('path', { + d: 'M3 3v5h5', + }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderSearchIcon24.ts b/fe-app-podkop/src/icons/renderSearchIcon24.ts new file mode 100644 index 0000000..3025384 --- /dev/null +++ b/fe-app-podkop/src/icons/renderSearchIcon24.ts @@ -0,0 +1,22 @@ +import { svgEl } from '../helpers'; + +export function renderSearchIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-search-icon lucide-search', + }, + [ + svgEl('path', { d: 'm21 21-4.34-4.34' }), + svgEl('circle', { cx: '11', cy: '11', r: '8' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts b/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts new file mode 100644 index 0000000..16c500e --- /dev/null +++ b/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts @@ -0,0 +1,30 @@ +import { svgEl } from '../helpers'; + +export function renderSquareChartGanttIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt', + }, + [ + svgEl('rect', { + width: '18', + height: '18', + x: '3', + y: '3', + rx: '2', + }), + svgEl('path', { d: 'M9 8h7' }), + svgEl('path', { d: 'M8 12h6' }), + svgEl('path', { d: 'M11 16h5' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts b/fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts new file mode 100644 index 0000000..993b317 --- /dev/null +++ b/fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts @@ -0,0 +1,25 @@ +import { svgEl } from '../helpers'; + +export function renderTriangleAlertIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-triangle-alert-icon lucide-triangle-alert', + }, + [ + svgEl('path', { + d: 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3', + }), + svgEl('path', { d: 'M12 9v4' }), + svgEl('path', { d: 'M12 17h.01' }), + ], + ); +} diff --git a/fe-app-podkop/src/icons/renderXIcon24.ts b/fe-app-podkop/src/icons/renderXIcon24.ts new file mode 100644 index 0000000..d8b6ce6 --- /dev/null +++ b/fe-app-podkop/src/icons/renderXIcon24.ts @@ -0,0 +1,19 @@ +import { svgEl } from '../helpers'; + +export function renderXIcon24() { + const NS = 'http://www.w3.org/2000/svg'; + return svgEl( + 'svg', + { + xmlns: NS, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + class: 'lucide lucide-x-icon lucide-x', + }, + [svgEl('path', { d: 'M18 6 6 18' }), svgEl('path', { d: 'm6 6 12 12' })], + ); +} diff --git a/fe-app-podkop/src/luci.d.ts b/fe-app-podkop/src/luci.d.ts index 01ff833..3600e76 100644 --- a/fe-app-podkop/src/luci.d.ts +++ b/fe-app-podkop/src/luci.d.ts @@ -35,6 +35,16 @@ declare global { }; const _ = (_key: string) => string; + + const ui = { + showModal: (_title: stirng, _content: HtmlElement) => undefined, + hideModal: () => undefined, + addNotification: ( + _title: string, + _children: HtmlElement | HtmlElement[], + _className?: string, + ) => undefined, + }; } export {}; diff --git a/fe-app-podkop/src/main.ts b/fe-app-podkop/src/main.ts index 7d7e2b7..bffa791 100644 --- a/fe-app-podkop/src/main.ts +++ b/fe-app-podkop/src/main.ts @@ -2,9 +2,9 @@ 'require baseclass'; 'require fs'; 'require uci'; +'require ui'; export * from './validators'; export * from './helpers'; -export * from './clash'; export * from './podkop'; export * from './constants'; diff --git a/fe-app-podkop/src/partials/button/renderButton.ts b/fe-app-podkop/src/partials/button/renderButton.ts new file mode 100644 index 0000000..5912cc4 --- /dev/null +++ b/fe-app-podkop/src/partials/button/renderButton.ts @@ -0,0 +1,69 @@ +import { insertIf } from '../../helpers'; +import { renderLoaderCircleIcon24 } from '../../icons'; + +interface IRenderButtonProps { + classNames?: string[]; + disabled?: boolean; + loading?: boolean; + icon?: () => SVGSVGElement; + onClick: () => void; + text: string; +} + +export function renderButton({ + classNames = [], + disabled, + loading, + onClick, + text, + icon, +}: IRenderButtonProps) { + const hasIcon = !!loading || !!icon; + + function getWrappedIcon() { + const iconWrap = E('span', { + class: 'pdk-partial-button__icon', + }); + + if (loading) { + iconWrap.appendChild(renderLoaderCircleIcon24()); + + return iconWrap; + } + + if (icon) { + iconWrap.appendChild(icon()); + + return iconWrap; + } + + return iconWrap; + } + + function getClass() { + return [ + 'btn', + 'pdk-partial-button', + ...insertIf(Boolean(disabled), ['pdk-partial-button--disabled']), + ...insertIf(Boolean(loading), ['pdk-partial-button--loading']), + ...insertIf(Boolean(hasIcon), ['pdk-partial-button--with-icon']), + ...classNames, + ] + .filter(Boolean) + .join(' '); + } + + function getDisabled() { + if (loading || disabled) { + return true; + } + + return undefined; + } + + return E( + 'button', + { class: getClass(), disabled: getDisabled(), click: onClick }, + [...insertIf(hasIcon, [getWrappedIcon()]), E('span', {}, text)], + ); +} diff --git a/fe-app-podkop/src/partials/button/styles.ts b/fe-app-podkop/src/partials/button/styles.ts new file mode 100644 index 0000000..777ef0b --- /dev/null +++ b/fe-app-podkop/src/partials/button/styles.ts @@ -0,0 +1,33 @@ +// language=CSS +export const styles = ` +.pdk-partial-button { + text-align: center; +} + +.pdk-partial-button--with-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button--loading { +} + +.pdk-partial-button--disabled { +} + +.pdk-partial-button__icon { + margin-right: 5px; +} + +.pdk-partial-button__icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button__icon svg { + width: 16px; + height: 16px; +} +`; diff --git a/fe-app-podkop/src/partials/index.ts b/fe-app-podkop/src/partials/index.ts new file mode 100644 index 0000000..fc5f5d8 --- /dev/null +++ b/fe-app-podkop/src/partials/index.ts @@ -0,0 +1,10 @@ +import { styles as ButtonStyles } from './button/styles'; +import { styles as ModalStyles } from './modal/styles'; + +export * from './button/renderButton'; +export * from './modal/renderModal'; + +export const PartialStyles = ` +${ButtonStyles} +${ModalStyles} +`; diff --git a/fe-app-podkop/src/partials/modal/renderModal.ts b/fe-app-podkop/src/partials/modal/renderModal.ts new file mode 100644 index 0000000..8c91742 --- /dev/null +++ b/fe-app-podkop/src/partials/modal/renderModal.ts @@ -0,0 +1,32 @@ +import { renderButton } from '../button/renderButton'; +import { copyToClipboard } from '../../helpers/copyToClipboard'; +import { downloadAsTxt } from '../../helpers/downloadAsTxt'; + +export function renderModal(text: string, name: string) { + return E( + 'div', + { class: 'pdk-partial-modal__body' }, + E('div', {}, [ + E('pre', { class: 'pdk-partial-modal__content' }, E('code', {}, text)), + + E('div', { class: 'pdk-partial-modal__footer' }, [ + renderButton({ + classNames: ['cbi-button-apply'], + text: _('Download'), + onClick: () => downloadAsTxt(text, name), + }), + renderButton({ + classNames: ['cbi-button-apply'], + text: _('Copy'), + onClick: () => + copyToClipboard(` \`\`\`${name} \n ${text} \n \`\`\``), + }), + renderButton({ + classNames: ['cbi-button-remove'], + text: _('Close'), + onClick: ui.hideModal, + }), + ]), + ]), + ); +} diff --git a/fe-app-podkop/src/partials/modal/styles.ts b/fe-app-podkop/src/partials/modal/styles.ts new file mode 100644 index 0000000..929e5ec --- /dev/null +++ b/fe-app-podkop/src/partials/modal/styles.ts @@ -0,0 +1,20 @@ +// language=CSS +export const styles = ` + +.pdk-partial-modal__body {} + +.pdk-partial-modal__content { + max-height: 70vh; + overflow: scroll; + border-radius: 4px; +} + +.pdk-partial-modal__footer { + display: flex; + justify-content: flex-end; +} + +.pdk-partial-modal__footer button { + margin-left: 10px; +} +`; diff --git a/fe-app-podkop/src/podkop/api.ts b/fe-app-podkop/src/podkop/api.ts new file mode 100644 index 0000000..5214c54 --- /dev/null +++ b/fe-app-podkop/src/podkop/api.ts @@ -0,0 +1,53 @@ +import { withTimeout } from '../helpers'; + +export async function createBaseApiRequest( + fetchFn: () => Promise, + options?: { + timeoutMs?: number; + operationName?: string; + timeoutMessage?: string; + }, +): Promise> { + const wrappedFn = () => + options?.timeoutMs && options?.operationName + ? withTimeout( + fetchFn(), + options.timeoutMs, + options.operationName, + options.timeoutMessage, + ) + : fetchFn(); + + try { + const response = await wrappedFn(); + + if (!response.ok) { + return { + success: false as const, + message: `${_('HTTP error')} ${response.status}: ${response.statusText}`, + }; + } + + const data: T = await response.json(); + + return { + success: true as const, + data, + }; + } catch (e) { + return { + success: false as const, + message: e instanceof Error ? e.message : _('Unknown error'), + }; + } +} + +export type IBaseApiResponse = + | { + success: true; + data: T; + } + | { + success: false; + message: string; + }; diff --git a/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts b/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts new file mode 100644 index 0000000..777ab94 --- /dev/null +++ b/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts @@ -0,0 +1,29 @@ +import { PodkopShellMethods } from '../methods'; +import { store } from '../services'; + +export async function fetchServicesInfo() { + const [podkop, singbox] = await Promise.all([ + PodkopShellMethods.getStatus(), + PodkopShellMethods.getSingBoxStatus(), + ]); + + if (!podkop.success || !singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, podkop: 0 }, + }, + }); + } + + if (podkop.success && singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.data.running, podkop: podkop.data.enabled }, + }, + }); + } +} diff --git a/fe-app-podkop/src/podkop/fetchers/index.ts b/fe-app-podkop/src/podkop/fetchers/index.ts new file mode 100644 index 0000000..9b58655 --- /dev/null +++ b/fe-app-podkop/src/podkop/fetchers/index.ts @@ -0,0 +1 @@ +export * from './fetchServicesInfo'; diff --git a/fe-app-podkop/src/podkop/methods/getConfigSections.ts b/fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts similarity index 79% rename from fe-app-podkop/src/podkop/methods/getConfigSections.ts rename to fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts index d8883d4..bdb13c4 100644 --- a/fe-app-podkop/src/podkop/methods/getConfigSections.ts +++ b/fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts @@ -1,4 +1,4 @@ -import { Podkop } from '../types'; +import { Podkop } from '../../types'; export async function getConfigSections(): Promise { return uci.load('podkop').then(() => uci.sections('podkop')); diff --git a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts similarity index 91% rename from fe-app-podkop/src/podkop/methods/getDashboardSections.ts rename to fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts index 28a5573..709d6a1 100644 --- a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts @@ -1,7 +1,7 @@ -import { Podkop } from '../types'; import { getConfigSections } from './getConfigSections'; -import { getClashProxies } from '../../clash'; -import { getProxyUrlName, splitProxyString } from '../../helpers'; +import { Podkop } from '../../types'; +import { getProxyUrlName, splitProxyString } from '../../../helpers'; +import { PodkopShellMethods } from '../shell'; interface IGetDashboardSectionsResponse { success: boolean; @@ -10,7 +10,7 @@ interface IGetDashboardSectionsResponse { export async function getDashboardSections(): Promise { const configSections = await getConfigSections(); - const clashProxies = await getClashProxies(); + const clashProxies = await PodkopShellMethods.getClashApiProxies(); if (!clashProxies.success) { return { @@ -27,9 +27,12 @@ export async function getDashboardSections(): Promise section.mode !== 'block') + .filter( + (section) => + section.connection_type !== 'block' && section['.type'] !== 'settings', + ) .map((section) => { - if (section.mode === 'proxy') { + if (section.connection_type === 'proxy') { if (section.proxy_config_type === 'url') { const outbound = proxies.find( (proxy) => proxy.code === `${section['.name']}-out`, @@ -122,7 +125,7 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, ); diff --git a/fe-app-podkop/src/podkop/methods/custom/index.ts b/fe-app-podkop/src/podkop/methods/custom/index.ts new file mode 100644 index 0000000..7ade0fa --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/custom/index.ts @@ -0,0 +1,7 @@ +import { getConfigSections } from './getConfigSections'; +import { getDashboardSections } from './getDashboardSections'; + +export const CustomPodkopMethods = { + getConfigSections, + getDashboardSections, +}; diff --git a/fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts b/fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts new file mode 100644 index 0000000..4044ca0 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts @@ -0,0 +1,23 @@ +import { FAKEIP_CHECK_DOMAIN } from '../../../constants'; +import { createBaseApiRequest, IBaseApiResponse } from '../../api'; + +interface IGetFakeIpCheckResponse { + fakeip: boolean; + IP: string; +} + +export async function getFakeIpCheck(): Promise< + IBaseApiResponse +> { + return createBaseApiRequest( + () => + fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }), + { + operationName: 'getFakeIpCheck', + timeoutMs: 5000, + }, + ); +} diff --git a/fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts b/fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts new file mode 100644 index 0000000..c25a60e --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts @@ -0,0 +1,23 @@ +import { IP_CHECK_DOMAIN } from '../../../constants'; +import { createBaseApiRequest, IBaseApiResponse } from '../../api'; + +interface IGetIpCheckResponse { + fakeip: boolean; + IP: string; +} + +export async function getIpCheck(): Promise< + IBaseApiResponse +> { + return createBaseApiRequest( + () => + fetch(`https://${IP_CHECK_DOMAIN}/check`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }), + { + operationName: 'getIpCheck', + timeoutMs: 5000, + }, + ); +} diff --git a/fe-app-podkop/src/podkop/methods/fakeip/index.ts b/fe-app-podkop/src/podkop/methods/fakeip/index.ts new file mode 100644 index 0000000..4a95f16 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/fakeip/index.ts @@ -0,0 +1,7 @@ +import { getFakeIpCheck } from './getFakeIpCheck'; +import { getIpCheck } from './getIpCheck'; + +export const RemoteFakeIPMethods = { + getFakeIpCheck, + getIpCheck, +}; diff --git a/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts b/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts deleted file mode 100644 index 666c41e..0000000 --- a/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { executeShellCommand } from '../../helpers'; - -export async function getPodkopStatus(): Promise<{ - enabled: number; - status: string; -}> { - const response = await executeShellCommand({ - command: '/usr/bin/podkop', - args: ['get_status'], - timeout: 10000, - }); - - if (response.stdout) { - return JSON.parse(response.stdout.replace(/\n/g, '')) as { - enabled: number; - status: string; - }; - } - - return { enabled: 0, status: 'unknown' }; -} diff --git a/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts b/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts deleted file mode 100644 index 41735f5..0000000 --- a/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { executeShellCommand } from '../../helpers'; - -export async function getSingboxStatus(): Promise<{ - running: number; - enabled: number; - status: string; -}> { - const response = await executeShellCommand({ - command: '/usr/bin/podkop', - args: ['get_sing_box_status'], - timeout: 10000, - }); - - if (response.stdout) { - return JSON.parse(response.stdout.replace(/\n/g, '')) as { - running: number; - enabled: number; - status: string; - }; - } - - return { running: 0, enabled: 0, status: 'unknown' }; -} diff --git a/fe-app-podkop/src/podkop/methods/index.ts b/fe-app-podkop/src/podkop/methods/index.ts index 6b2c1f3..fca93a4 100644 --- a/fe-app-podkop/src/podkop/methods/index.ts +++ b/fe-app-podkop/src/podkop/methods/index.ts @@ -1,4 +1,3 @@ -export * from './getConfigSections'; -export * from './getDashboardSections'; -export * from './getPodkopStatus'; -export * from './getSingboxStatus'; +export * from './custom'; +export * from './fakeip'; +export * from './shell'; diff --git a/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts b/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts new file mode 100644 index 0000000..04f1ef9 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts @@ -0,0 +1,33 @@ +import { executeShellCommand } from '../../../helpers'; +import { Podkop } from '../../types'; + +export async function callBaseMethod( + method: Podkop.AvailableMethods, + args: string[] = [], + command: string = '/usr/bin/podkop', +): Promise> { + const response = await executeShellCommand({ + command, + args: [method as string, ...args], + timeout: 10000, + }); + + if (response.stdout) { + try { + return { + success: true, + data: JSON.parse(response.stdout) as T, + }; + } catch (_e) { + return { + success: true, + data: response.stdout as T, + }; + } + } + + return { + success: false, + error: '', + }; +} diff --git a/fe-app-podkop/src/podkop/methods/shell/index.ts b/fe-app-podkop/src/podkop/methods/shell/index.ts new file mode 100644 index 0000000..1a6fe7c --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/shell/index.ts @@ -0,0 +1,87 @@ +import { callBaseMethod } from './callBaseMethod'; +import { ClashAPI, Podkop } from '../../types'; + +export const PodkopShellMethods = { + checkDNSAvailable: async () => + callBaseMethod( + Podkop.AvailableMethods.CHECK_DNS_AVAILABLE, + ), + checkFakeIP: async () => + callBaseMethod( + Podkop.AvailableMethods.CHECK_FAKEIP, + ), + checkNftRules: async () => + callBaseMethod( + Podkop.AvailableMethods.CHECK_NFT_RULES, + ), + getStatus: async () => + callBaseMethod(Podkop.AvailableMethods.GET_STATUS), + checkSingBox: async () => + callBaseMethod( + Podkop.AvailableMethods.CHECK_SING_BOX, + ), + getSingBoxStatus: async () => + callBaseMethod( + Podkop.AvailableMethods.GET_SING_BOX_STATUS, + ), + getClashApiProxies: async () => + callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_PROXIES, + ]), + getClashApiProxyLatency: async (tag: string) => + callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, + tag, + ]), + getClashApiGroupLatency: async (tag: string) => + callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, + tag, + ]), + setClashApiGroupProxy: async (group: string, proxy: string) => + callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY, + group, + proxy, + ]), + restart: async () => + callBaseMethod( + Podkop.AvailableMethods.RESTART, + [], + '/etc/init.d/podkop', + ), + start: async () => + callBaseMethod( + Podkop.AvailableMethods.START, + [], + '/etc/init.d/podkop', + ), + stop: async () => + callBaseMethod( + Podkop.AvailableMethods.STOP, + [], + '/etc/init.d/podkop', + ), + enable: async () => + callBaseMethod( + Podkop.AvailableMethods.ENABLE, + [], + '/etc/init.d/podkop', + ), + disable: async () => + callBaseMethod( + Podkop.AvailableMethods.DISABLE, + [], + '/etc/init.d/podkop', + ), + globalCheck: async () => + callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), + showSingBoxConfig: async () => + callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), + checkLogs: async () => + callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), + getSystemInfo: async () => + callBaseMethod( + Podkop.AvailableMethods.GET_SYSTEM_INFO, + ), +}; diff --git a/fe-app-podkop/src/podkop/services/core.service.ts b/fe-app-podkop/src/podkop/services/core.service.ts index 4b7d827..79b63db 100644 --- a/fe-app-podkop/src/podkop/services/core.service.ts +++ b/fe-app-podkop/src/podkop/services/core.service.ts @@ -1,8 +1,12 @@ import { TabServiceInstance } from './tab.service'; -import { store } from '../../store'; +import { store } from './store.service'; +import { logger } from './logger.service'; +import { PodkopLogWatcher } from './podkopLogWatcher.service'; +import { PodkopShellMethods } from '../methods'; export function coreService() { TabServiceInstance.onChange((activeId, tabs) => { + logger.info('[TAB]', activeId); store.set({ tabService: { current: activeId || '', @@ -10,4 +14,31 @@ export function coreService() { }, }); }); + + const watcher = PodkopLogWatcher.getInstance(); + + watcher.init( + async () => { + const logs = await PodkopShellMethods.checkLogs(); + + if (logs.success) { + return logs.data as string; + } + + return ''; + }, + { + intervalMs: 3000, + onNewLog: (line) => { + if ( + line.toLowerCase().includes('[error]') || + line.toLowerCase().includes('[fatal]') + ) { + ui.addNotification('Podkop Error', E('div', {}, line), 'error'); + } + }, + }, + ); + + watcher.start(); } diff --git a/fe-app-podkop/src/podkop/services/index.ts b/fe-app-podkop/src/podkop/services/index.ts index 4b776d2..e3160a8 100644 --- a/fe-app-podkop/src/podkop/services/index.ts +++ b/fe-app-podkop/src/podkop/services/index.ts @@ -1,2 +1,5 @@ export * from './tab.service'; export * from './core.service'; +export * from './socket.service'; +export * from './store.service'; +export * from './logger.service'; diff --git a/fe-app-podkop/src/podkop/services/logger.service.ts b/fe-app-podkop/src/podkop/services/logger.service.ts new file mode 100644 index 0000000..ecc0ff0 --- /dev/null +++ b/fe-app-podkop/src/podkop/services/logger.service.ts @@ -0,0 +1,66 @@ +import { downloadAsTxt } from '../../helpers/downloadAsTxt'; + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export class Logger { + private logs: string[] = []; + private readonly levels: LogLevel[] = ['debug', 'info', 'warn', 'error']; + + private format(level: LogLevel, ...args: unknown[]): string { + return `[${level.toUpperCase()}] ${args.join(' ')}`; + } + + private push(level: LogLevel, ...args: unknown[]): void { + if (!this.levels.includes(level)) level = 'info'; + const message = this.format(level, ...args); + this.logs.push(message); + + switch (level) { + case 'error': + console.error(message); + break; + case 'warn': + console.warn(message); + break; + case 'info': + console.info(message); + break; + default: + console.log(message); + } + } + + debug(...args: unknown[]): void { + this.push('debug', ...args); + } + + info(...args: unknown[]): void { + this.push('info', ...args); + } + + warn(...args: unknown[]): void { + this.push('warn', ...args); + } + + error(...args: unknown[]): void { + this.push('error', ...args); + } + + clear(): void { + this.logs = []; + } + + getLogs(): string { + return this.logs.join('\n'); + } + + download(filename = 'logs.txt'): void { + if (typeof document === 'undefined') { + console.warn('Logger.download() доступен только в браузере'); + return; + } + downloadAsTxt(this.getLogs(), filename); + } +} + +export const logger = new Logger(); diff --git a/fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts b/fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts new file mode 100644 index 0000000..e09b288 --- /dev/null +++ b/fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts @@ -0,0 +1,116 @@ +import { logger } from './logger.service'; + +export type LogFetcher = () => Promise | string; + +export interface PodkopLogWatcherOptions { + intervalMs?: number; + onNewLog?: (line: string) => void; +} + +export class PodkopLogWatcher { + private static instance: PodkopLogWatcher; + private fetcher?: LogFetcher; + private onNewLog?: (line: string) => void; + private intervalMs = 5000; + private lastLines = new Set(); + private timer?: ReturnType; + private running = false; + private paused = false; + + private constructor() { + if (typeof document !== 'undefined') { + document.addEventListener('visibilitychange', () => { + if (document.hidden) this.pause(); + else this.resume(); + }); + } + } + + static getInstance(): PodkopLogWatcher { + if (!PodkopLogWatcher.instance) { + PodkopLogWatcher.instance = new PodkopLogWatcher(); + } + return PodkopLogWatcher.instance; + } + + init(fetcher: LogFetcher, options?: PodkopLogWatcherOptions): void { + this.fetcher = fetcher; + this.onNewLog = options?.onNewLog; + this.intervalMs = options?.intervalMs ?? 5000; + logger.info( + '[PodkopLogWatcher]', + `initialized (interval: ${this.intervalMs}ms)`, + ); + } + + async checkOnce(): Promise { + if (!this.fetcher) { + logger.warn('[PodkopLogWatcher]', 'fetcher not found'); + return; + } + + if (this.paused) { + logger.debug('[PodkopLogWatcher]', 'skipped check — tab not visible'); + return; + } + + try { + const raw = await this.fetcher(); + const lines = raw.split('\n').filter(Boolean); + + for (const line of lines) { + if (!this.lastLines.has(line)) { + this.lastLines.add(line); + this.onNewLog?.(line); + } + } + + if (this.lastLines.size > 500) { + const arr = Array.from(this.lastLines); + this.lastLines = new Set(arr.slice(-500)); + } + } catch (err) { + logger.error('[PodkopLogWatcher]', 'failed to read logs:', err); + } + } + + start(): void { + if (this.running) return; + if (!this.fetcher) { + logger.warn('[PodkopLogWatcher]', 'attempted to start without fetcher'); + return; + } + + this.running = true; + this.timer = setInterval(() => this.checkOnce(), this.intervalMs); + logger.info( + '[PodkopLogWatcher]', + `started (interval: ${this.intervalMs}ms)`, + ); + } + + stop(): void { + if (!this.running) return; + this.running = false; + if (this.timer) clearInterval(this.timer); + logger.info('[PodkopLogWatcher]', 'stopped'); + } + + pause(): void { + if (!this.running || this.paused) return; + this.paused = true; + logger.info('[PodkopLogWatcher]', 'paused (tab not visible)'); + } + + resume(): void { + if (!this.running || !this.paused) return; + this.paused = false; + logger.info('[PodkopLogWatcher]', 'resumed (tab active)'); + this.checkOnce(); // сразу проверить, не появились ли новые логи + } + + reset(): void { + this.lastLines.clear(); + logger.info('[PodkopLogWatcher]', 'log history reset'); + } +} diff --git a/fe-app-podkop/src/socket.ts b/fe-app-podkop/src/podkop/services/socket.service.ts similarity index 65% rename from fe-app-podkop/src/socket.ts rename to fe-app-podkop/src/podkop/services/socket.service.ts index 5a401b8..2a21091 100644 --- a/fe-app-podkop/src/socket.ts +++ b/fe-app-podkop/src/podkop/services/socket.service.ts @@ -1,3 +1,5 @@ +import { logger } from './logger.service'; + // eslint-disable-next-line type Listener = (data: any) => void; type ErrorListener = (error: Event | string) => void; @@ -18,10 +20,48 @@ class SocketManager { return SocketManager.instance; } + resetAll(): void { + for (const [url, ws] of this.sockets.entries()) { + try { + if ( + ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING + ) { + ws.close(); + } + } catch (err) { + logger.error( + '[SOCKET]', + `resetAll: failed to close socket ${url}`, + err, + ); + } + } + + this.sockets.clear(); + this.listeners.clear(); + this.errorListeners.clear(); + this.connected.clear(); + logger.info('[SOCKET]', 'All connections and state have been reset.'); + } + connect(url: string): void { if (this.sockets.has(url)) return; - const ws = new WebSocket(url); + let ws: WebSocket; + + try { + ws = new WebSocket(url); + } catch (err) { + logger.error( + '[SOCKET]', + `failed to construct WebSocket for ${url}:`, + err, + ); + this.triggerError(url, err instanceof Event ? err : String(err)); + return; + } + this.sockets.set(url, ws); this.connected.set(url, false); this.listeners.set(url, new Set()); @@ -29,7 +69,7 @@ class SocketManager { ws.addEventListener('open', () => { this.connected.set(url, true); - console.info(`Connected: ${url}`); + logger.info('[SOCKET]', 'Connected to', url); }); ws.addEventListener('message', (event) => { @@ -39,7 +79,7 @@ class SocketManager { try { handler(event.data); } catch (err) { - console.error(`Handler error for ${url}:`, err); + logger.error('[SOCKET]', `Handler error for ${url}:`, err); } } } @@ -47,26 +87,32 @@ class SocketManager { ws.addEventListener('close', () => { this.connected.set(url, false); - console.warn(`Disconnected: ${url}`); + logger.warn('[SOCKET]', `Disconnected: ${url}`); this.triggerError(url, 'Connection closed'); }); ws.addEventListener('error', (err) => { - console.error(`Socket error for ${url}:`, err); + logger.error('[SOCKET]', `Socket error for ${url}:`, err); this.triggerError(url, err); }); } subscribe(url: string, listener: Listener, onError?: ErrorListener): void { + if (!this.errorListeners.has(url)) { + this.errorListeners.set(url, new Set()); + } + if (onError) { + this.errorListeners.get(url)?.add(onError); + } + if (!this.sockets.has(url)) { this.connect(url); } - this.listeners.get(url)?.add(listener); - - if (onError) { - this.errorListeners.get(url)?.add(onError); + if (!this.listeners.has(url)) { + this.listeners.set(url, new Set()); } + this.listeners.get(url)?.add(listener); } unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void { @@ -82,7 +128,7 @@ class SocketManager { if (ws && this.connected.get(url)) { ws.send(typeof data === 'string' ? data : JSON.stringify(data)); } else { - console.warn(`Cannot send: not connected to ${url}`); + logger.warn('[SOCKET]', `Cannot send: not connected to ${url}`); this.triggerError(url, 'Not connected'); } } @@ -111,7 +157,7 @@ class SocketManager { try { cb(err); } catch (e) { - console.error(`Error handler threw for ${url}:`, e); + logger.error('[SOCKET]', `Error handler threw for ${url}:`, e); } } } diff --git a/fe-app-podkop/src/store.ts b/fe-app-podkop/src/podkop/services/store.service.ts similarity index 71% rename from fe-app-podkop/src/store.ts rename to fe-app-podkop/src/podkop/services/store.service.ts index 4591353..4069240 100644 --- a/fe-app-podkop/src/store.ts +++ b/fe-app-podkop/src/podkop/services/store.service.ts @@ -1,4 +1,5 @@ -import { Podkop } from './podkop/types'; +import { Podkop } from '../types'; +import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store'; function jsonStableStringify(obj: T): string { return JSON.stringify(obj, (_, value) => { @@ -28,7 +29,7 @@ function jsonEqual(a: A, b: B): boolean { type Listener = (next: T, prev: T, diff: Partial) => void; // eslint-disable-next-line -class Store> { +class StoreService> { private value: T; private readonly initial: T; private listeners = new Set>(); @@ -61,9 +62,17 @@ class Store> { this.listeners.forEach((cb) => cb(this.value, prev, diff)); } - reset(): void { + reset(keys?: K[]): void { const prev = this.value; - const next = structuredClone(this.initial); + const next = structuredClone(this.value); + + if (keys && keys.length > 0) { + keys.forEach((key) => { + next[key] = structuredClone(this.initial[key]); + }); + } else { + Object.assign(next, structuredClone(this.initial)); + } if (jsonEqual(prev, next)) return; @@ -112,6 +121,21 @@ class Store> { } } +export interface IDiagnosticsChecksItem { + state: 'error' | 'warning' | 'success'; + key: string; + value: string; +} + +export interface IDiagnosticsChecksStoreItem { + order: number; + code: string; + title: string; + description: string; + state: 'loading' | 'warning' | 'success' | 'error' | 'skipped'; + items: Array; +} + export interface StoreType { tabService: { current: string; @@ -143,6 +167,29 @@ export interface StoreType { data: Podkop.OutboundGroup[]; latencyFetching: boolean; }; + diagnosticsRunAction: { + loading: boolean; + }; + diagnosticsChecks: Array; + diagnosticsActions: { + restart: { loading: boolean }; + start: { loading: boolean }; + stop: { loading: boolean }; + enable: { loading: boolean }; + disable: { loading: boolean }; + globalCheck: { loading: boolean }; + viewLogs: { loading: boolean }; + showSingBoxConfig: { loading: boolean }; + }; + diagnosticsSystemInfo: { + loading: boolean; + podkop_version: string; + podkop_latest_version: string; + luci_app_version: string; + sing_box_version: string; + openwrt_version: string; + device_model: string; + }; } const initialStore: StoreType = { @@ -176,6 +223,7 @@ const initialStore: StoreType = { latencyFetching: false, data: [], }, + ...initialDiagnosticStore, }; -export const store = new Store(initialStore); +export const store = new StoreService(initialStore); diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/index.ts b/fe-app-podkop/src/podkop/tabs/dashboard/index.ts index 898949a..7540731 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/index.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/index.ts @@ -1,2 +1,9 @@ -export * from './renderDashboard'; -export * from './initDashboardController'; +import { render } from './render'; +import { initController } from './initController'; +import { styles } from './styles'; + +export const DashboardTab = { + render, + initController, + styles, +}; diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts b/fe-app-podkop/src/podkop/tabs/dashboard/initController.ts similarity index 77% rename from fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts rename to fe-app-podkop/src/podkop/tabs/dashboard/initController.ts index d5c9526..e1abeab 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/initController.ts @@ -1,24 +1,13 @@ import { - getDashboardSections, - getPodkopStatus, - getSingboxStatus, -} from '../../methods'; -import { - getClashApiUrl, getClashWsUrl, onMount, preserveScrollForPage, } from '../../../helpers'; -import { - triggerLatencyGroupTest, - triggerLatencyProxyTest, - triggerProxySelector, -} from '../../../clash'; -import { store, StoreType } from '../../../store'; -import { socket } from '../../../socket'; import { prettyBytes } from '../../../helpers/prettyBytes'; -import { renderSections } from './renderSections'; -import { renderWidget } from './renderWidget'; +import { CustomPodkopMethods, PodkopShellMethods } from '../../methods'; +import { logger, socket, store, StoreType } from '../../services'; +import { renderSections, renderWidget } from './partials'; +import { fetchServicesInfo } from '../../fetchers'; // Fetchers @@ -32,10 +21,10 @@ async function fetchDashboardSections() { }, }); - const { data, success } = await getDashboardSections(); + const { data, success } = await CustomPodkopMethods.getDashboardSections(); if (!success) { - console.log('[fetchDashboardSections]: failed to fetch', getClashApiUrl()); + logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch'); } store.set({ @@ -48,33 +37,6 @@ async function fetchDashboardSections() { }); } -async function fetchServicesInfo() { - try { - const [podkop, singbox] = await Promise.all([ - getPodkopStatus(), - getSingboxStatus(), - ]); - - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { singbox: singbox.running, podkop: podkop.enabled }, - }, - }); - } catch (err) { - console.log('[fetchServicesInfo]: failed to fetchServices', err); - - store.set({ - servicesInfoWidget: { - loading: false, - failed: true, - data: { singbox: 0, podkop: 0 }, - }, - }); - } -} - async function connectToClashSockets() { socket.subscribe( `${getClashWsUrl()}/traffic?token=`, @@ -90,8 +52,9 @@ async function connectToClashSockets() { }); }, (_err) => { - console.log( - '[fetchDashboardSections]: failed to connect', + logger.error( + '[DASHBOARD]', + 'connectToClashSockets - traffic: failed to connect to', getClashWsUrl(), ); store.set({ @@ -129,8 +92,9 @@ async function connectToClashSockets() { }); }, (_err) => { - console.log( - '[fetchDashboardSections]: failed to connect', + logger.error( + '[DASHBOARD]', + 'connectToClashSockets - connections: failed to connect to', getClashWsUrl(), ); store.set({ @@ -155,7 +119,7 @@ async function connectToClashSockets() { // Handlers async function handleChooseOutbound(selector: string, tag: string) { - await triggerProxySelector(selector, tag); + await PodkopShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections(); } @@ -167,7 +131,7 @@ async function handleTestGroupLatency(tag: string) { }, }); - await triggerLatencyGroupTest(tag); + await PodkopShellMethods.getClashApiGroupLatency(tag); await fetchDashboardSections(); store.set({ @@ -186,7 +150,7 @@ async function handleTestProxyLatency(tag: string) { }, }); - await triggerLatencyProxyTest(tag); + await PodkopShellMethods.getClashApiProxyLatency(tag); await fetchDashboardSections(); store.set({ @@ -200,7 +164,7 @@ async function handleTestProxyLatency(tag: string) { // Renderer async function renderSectionsWidget() { - console.log('renderSectionsWidget'); + logger.debug('[DASHBOARD]', 'renderSectionsWidget'); const sectionsWidget = store.get().sectionsWidget; const container = document.getElementById('dashboard-sections-grid'); @@ -249,7 +213,7 @@ async function renderSectionsWidget() { } async function renderBandwidthWidget() { - console.log('renderBandwidthWidget'); + logger.debug('[DASHBOARD]', 'renderBandwidthWidget'); const traffic = store.get().bandwidthWidget; const container = document.getElementById('dashboard-widget-traffic'); @@ -279,7 +243,7 @@ async function renderBandwidthWidget() { } async function renderTrafficTotalWidget() { - console.log('renderTrafficTotalWidget'); + logger.debug('[DASHBOARD]', 'renderTrafficTotalWidget'); const trafficTotalWidget = store.get().trafficTotalWidget; const container = document.getElementById('dashboard-widget-traffic-total'); @@ -315,7 +279,7 @@ async function renderTrafficTotalWidget() { } async function renderSystemInfoWidget() { - console.log('renderSystemInfoWidget'); + logger.debug('[DASHBOARD]', 'renderSystemInfoWidget'); const systemInfoWidget = store.get().systemInfoWidget; const container = document.getElementById('dashboard-widget-system-info'); @@ -351,7 +315,7 @@ async function renderSystemInfoWidget() { } async function renderServicesInfoWidget() { - console.log('renderServicesInfoWidget'); + logger.debug('[DASHBOARD]', 'renderServicesInfoWidget'); const servicesInfoWidget = store.get().servicesInfoWidget; const container = document.getElementById('dashboard-widget-service-info'); @@ -426,19 +390,71 @@ async function onStoreUpdate( } } -export async function initDashboardController(): Promise { - onMount('dashboard-status').then(() => { - // Remove old listener - store.unsubscribe(onStoreUpdate); - // Clear store - store.reset(); +async function onPageMount() { + // Cleanup before mount + onPageUnmount(); - // Add new listener - store.subscribe(onStoreUpdate); + // Add new listener + store.subscribe(onStoreUpdate); - // Initial sections fetch - fetchDashboardSections(); - fetchServicesInfo(); - connectToClashSockets(); + // Initial sections fetch + await fetchDashboardSections(); + await fetchServicesInfo(); + await connectToClashSockets(); +} + +function onPageUnmount() { + // Remove old listener + store.unsubscribe(onStoreUpdate); + // Clear store + store.reset([ + 'bandwidthWidget', + 'trafficTotalWidget', + 'systemInfoWidget', + 'servicesInfoWidget', + 'sectionsWidget', + ]); + socket.resetAll(); +} + +function registerLifecycleListeners() { + store.subscribe((next, prev, diff) => { + if ( + diff.tabService && + next.tabService.current !== prev.tabService.current + ) { + logger.debug( + '[DASHBOARD]', + 'active tab diff event, active tab:', + diff.tabService.current, + ); + const isDashboardVisible = next.tabService.current === 'dashboard'; + + if (isDashboardVisible) { + logger.debug( + '[DASHBOARD]', + 'registerLifecycleListeners', + 'onPageMount', + ); + return onPageMount(); + } + + if (!isDashboardVisible) { + logger.debug( + '[DASHBOARD]', + 'registerLifecycleListeners', + 'onPageUnmount', + ); + return onPageUnmount(); + } + } + }); +} + +export async function initController(): Promise { + onMount('dashboard-status').then(() => { + logger.debug('[DASHBOARD]', 'initController', 'onMount'); + onPageMount(); + registerLifecycleListeners(); }); } diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/partials/index.ts b/fe-app-podkop/src/podkop/tabs/dashboard/partials/index.ts new file mode 100644 index 0000000..f797e31 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/partials/index.ts @@ -0,0 +1,2 @@ +export * from './renderSections'; +export * from './renderWidget'; diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts b/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts similarity index 92% rename from fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts rename to fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts index b8b77b0..bf78e7e 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts @@ -1,5 +1,4 @@ -import { Podkop } from '../../types'; -import { getClashApiUrl } from '../../../helpers'; +import { Podkop } from '../../../types'; interface IRenderSectionsProps { loading: boolean; @@ -17,10 +16,7 @@ function renderFailedState() { class: 'pdk_dashboard-page__outbound-section centered', style: 'height: 127px', }, - E('span', {}, [ - E('span', {}, _('Dashboard currently unavailable')), - E('div', { style: 'text-align: center;' }, `API: ${getClashApiUrl()}`), - ]), + E('span', {}, [E('span', {}, _('Dashboard currently unavailable'))]), ); } diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts b/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderWidget.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts rename to fe-app-podkop/src/podkop/tabs/dashboard/partials/renderWidget.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts b/fe-app-podkop/src/podkop/tabs/dashboard/render.ts similarity index 90% rename from fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts rename to fe-app-podkop/src/podkop/tabs/dashboard/render.ts index b4151e2..1417740 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/render.ts @@ -1,7 +1,6 @@ -import { renderSections } from './renderSections'; -import { renderWidget } from './renderWidget'; +import { renderSections, renderWidget } from './partials'; -export function renderDashboard() { +export function render() { return E( 'div', { @@ -47,6 +46,7 @@ export function renderDashboard() { }, onTestLatency: () => {}, onChooseOutbound: () => {}, + latencyFetching: false, }), ), ], diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/styles.ts b/fe-app-podkop/src/podkop/tabs/dashboard/styles.ts new file mode 100644 index 0000000..32066e5 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/styles.ts @@ -0,0 +1,120 @@ +// language=CSS +export const styles = ` +#cbi-podkop-dashboard-_mount_node > div { + width: 100%; +} + +#cbi-podkop-dashboard > h3 { + display: none; +} + +.pdk_dashboard-page { + width: 100%; + --dashboard-grid-columns: 4; +} + +@media (max-width: 900px) { + .pdk_dashboard-page { + --dashboard-grid-columns: 2; + } +} + +.pdk_dashboard-page__widgets-section { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; +} + +.pdk_dashboard-page__widgets-section__item { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; +} + +.pdk_dashboard-page__widgets-section__item__title {} + +.pdk_dashboard-page__widgets-section__item__row {} + +.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--error-color-medium, red); +} + +.pdk_dashboard-page__widgets-section__item__row__key {} + +.pdk_dashboard-page__widgets-section__item__row__value {} + +.pdk_dashboard-page__outbound-section { + margin-top: 10px; + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; +} + +.pdk_dashboard-page__outbound-section__title-section { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pdk_dashboard-page__outbound-section__title-section__title { + color: var(--text-color-high); + font-weight: 700; +} + +.pdk_dashboard-page__outbound-grid { + margin-top: 5px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; +} + +.pdk_dashboard-page__outbound-grid__item { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + transition: border 0.2s ease; +} + +.pdk_dashboard-page__outbound-grid__item--selectable { + cursor: pointer; +} + +.pdk_dashboard-page__outbound-grid__item--selectable:hover { + border-color: var(--primary-color-high, dodgerblue); +} + +.pdk_dashboard-page__outbound-grid__item--active { + border-color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__outbound-grid__item__footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; +} + +.pdk_dashboard-page__outbound-grid__item__type {} + +.pdk_dashboard-page__outbound-grid__item__latency--empty { + color: var(--primary-color-low, lightgray); +} + +.pdk_dashboard-page__outbound-grid__item__latency--green { + color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__outbound-grid__item__latency--yellow { + color: var(--warn-color-medium, orange); +} + +.pdk_dashboard-page__outbound-grid__item__latency--red { + color: var(--error-color-medium, red); +} + +`; diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/contstants.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/contstants.ts new file mode 100644 index 0000000..5c3b2d9 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/contstants.ts @@ -0,0 +1,32 @@ +export enum DIAGNOSTICS_CHECKS { + DNS = 'DNS', + SINGBOX = 'SINGBOX', + NFT = 'NFT', + FAKEIP = 'FAKEIP', +} + +export const DIAGNOSTICS_CHECKS_MAP: Record< + DIAGNOSTICS_CHECKS, + { order: number; title: string; code: DIAGNOSTICS_CHECKS } +> = { + [DIAGNOSTICS_CHECKS.DNS]: { + order: 1, + title: _('DNS checks'), + code: DIAGNOSTICS_CHECKS.DNS, + }, + [DIAGNOSTICS_CHECKS.SINGBOX]: { + order: 2, + title: _('Sing-box checks'), + code: DIAGNOSTICS_CHECKS.SINGBOX, + }, + [DIAGNOSTICS_CHECKS.NFT]: { + order: 3, + title: _('Nftables checks'), + code: DIAGNOSTICS_CHECKS.NFT, + }, + [DIAGNOSTICS_CHECKS.FAKEIP]: { + order: 4, + title: _('FakeIP checks'), + code: DIAGNOSTICS_CHECKS.FAKEIP, + }, +}; diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts new file mode 100644 index 0000000..c64cf7b --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts @@ -0,0 +1,98 @@ +import { insertIf } from '../../../../helpers'; +import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; +import { PodkopShellMethods } from '../../../methods'; +import { IDiagnosticsChecksItem } from '../../../services'; +import { updateCheckStore } from './updateCheckStore'; + +export async function runDnsCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; + + updateCheckStore({ + order, + code, + title, + description: _('Checking dns, please wait'), + state: 'loading', + items: [], + }); + + const dnsChecks = await PodkopShellMethods.checkDNSAvailable(); + + if (!dnsChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _('Cannot receive DNS checks result'), + state: 'error', + items: [], + }); + + throw new Error('DNS checks failed'); + } + + const data = dnsChecks.data; + + const allGood = + Boolean(data.dns_on_router) && + Boolean(data.dhcp_config_status) && + Boolean(data.bootstrap_dns_status) && + Boolean(data.dns_status); + + const atLeastOneGood = + Boolean(data.dns_on_router) || + Boolean(data.dhcp_config_status) || + Boolean(data.bootstrap_dns_status) || + Boolean(data.dns_status); + + function getStatus() { + if (allGood) { + return 'success'; + } + + if (atLeastOneGood) { + return 'warning'; + } + + return 'error'; + } + + updateCheckStore({ + order, + code, + title, + description: _('DNS checks passed'), + state: getStatus(), + items: [ + ...insertIf( + data.dns_type === 'doh' || data.dns_type === 'dot', + [ + { + state: data.bootstrap_dns_status ? 'success' : 'error', + key: _('Bootsrap DNS'), + value: data.bootstrap_dns_server, + }, + ], + ), + { + state: data.dns_status ? 'success' : 'error', + key: _('Main DNS'), + value: `${data.dns_server} [${data.dns_type}]`, + }, + { + state: data.dns_on_router ? 'success' : 'error', + key: _('DNS on router'), + value: '', + }, + { + state: data.dhcp_config_status ? 'success' : 'error', + key: _('DHCP has DNS server'), + value: '', + }, + ], + }); + + if (!atLeastOneGood) { + throw new Error('DNS checks failed'); + } +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts new file mode 100644 index 0000000..780a547 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts @@ -0,0 +1,95 @@ +import { insertIf } from '../../../../helpers'; +import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; +import { PodkopShellMethods, RemoteFakeIPMethods } from '../../../methods'; +import { IDiagnosticsChecksItem } from '../../../services'; +import { updateCheckStore } from './updateCheckStore'; + +export async function runFakeIPCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; + + updateCheckStore({ + order, + code, + title, + description: _('Checking FakeIP, please wait'), + state: 'loading', + items: [], + }); + + const routerFakeIPResponse = await PodkopShellMethods.checkFakeIP(); + const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); + const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); + + const checks = { + router: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, + browserFakeIP: + checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, + differentIP: + checkFakeIPResponse.success && + checkIPResponse.success && + checkFakeIPResponse.data.IP !== checkIPResponse.data.IP, + }; + + const allGood = checks.router || checks.browserFakeIP || checks.differentIP; + const atLeastOneGood = + checks.router && checks.browserFakeIP && checks.differentIP; + + function getMeta(): { + description: string; + state: 'loading' | 'warning' | 'success' | 'error' | 'skipped'; + } { + if (allGood) { + return { + state: 'success', + description: _('FakeIP checks passed'), + }; + } + + if (atLeastOneGood) { + return { + state: 'warning', + description: _('FakeIP checks partially passed'), + }; + } + + return { + state: 'error', + description: _('FakeIP checks failed'), + }; + } + + const { state, description } = getMeta(); + + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + { + state: checks.router ? 'success' : 'warning', + key: checks.router + ? _('Router DNS is routed through sing-box') + : _('Router DNS is not routed through sing-box'), + value: '', + }, + { + state: checks.browserFakeIP ? 'success' : 'error', + key: checks.browserFakeIP + ? _('Browser is using FakeIP correctly') + : _('Browser is not using FakeIP'), + value: '', + }, + ...insertIf(checks.browserFakeIP, [ + { + state: checks.differentIP ? 'success' : 'error', + key: checks.differentIP + ? _('Proxy traffic is routed via FakeIP') + : _('Proxy traffic is not routed via FakeIP'), + value: '', + }, + ]), + ], + }); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts new file mode 100644 index 0000000..5f413c9 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts @@ -0,0 +1,126 @@ +import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; +import { RemoteFakeIPMethods, PodkopShellMethods } from '../../../methods'; +import { updateCheckStore } from './updateCheckStore'; + +export async function runNftCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; + + updateCheckStore({ + order, + code, + title, + description: _('Checking nftables, please wait'), + state: 'loading', + items: [], + }); + + await RemoteFakeIPMethods.getFakeIpCheck(); + await RemoteFakeIPMethods.getIpCheck(); + + const nftablesChecks = await PodkopShellMethods.checkNftRules(); + + if (!nftablesChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _('Cannot receive nftables checks result'), + state: 'error', + items: [], + }); + + throw new Error('Nftables checks failed'); + } + + const data = nftablesChecks.data; + + const allGood = + Boolean(data.table_exist) && + Boolean(data.rules_mangle_exist) && + Boolean(data.rules_mangle_counters) && + Boolean(data.rules_mangle_output_exist) && + Boolean(data.rules_mangle_output_counters) && + Boolean(data.rules_proxy_exist) && + Boolean(data.rules_proxy_counters) && + !data.rules_other_mark_exist; + + const atLeastOneGood = + Boolean(data.table_exist) || + Boolean(data.rules_mangle_exist) || + Boolean(data.rules_mangle_counters) || + Boolean(data.rules_mangle_output_exist) || + Boolean(data.rules_mangle_output_counters) || + Boolean(data.rules_proxy_exist) || + Boolean(data.rules_proxy_counters) || + !data.rules_other_mark_exist; + + function getStatus() { + if (allGood) { + return 'success'; + } + + if (atLeastOneGood) { + return 'warning'; + } + + return 'error'; + } + + updateCheckStore({ + order, + code, + title, + description: allGood + ? _('Nftables checks passed') + : _('Nftables checks partially passed'), + state: getStatus(), + items: [ + { + state: data.table_exist ? 'success' : 'error', + key: _('Table exist'), + value: '', + }, + { + state: data.rules_mangle_exist ? 'success' : 'error', + key: _('Rules mangle exist'), + value: '', + }, + { + state: data.rules_mangle_counters ? 'success' : 'error', + key: _('Rules mangle counters'), + value: '', + }, + { + state: data.rules_mangle_output_exist ? 'success' : 'error', + key: _('Rules mangle output exist'), + value: '', + }, + { + state: data.rules_mangle_output_counters ? 'success' : 'error', + key: _('Rules mangle output counters'), + value: '', + }, + { + state: data.rules_proxy_exist ? 'success' : 'error', + key: _('Rules proxy exist'), + value: '', + }, + { + state: data.rules_proxy_counters ? 'success' : 'error', + key: _('Rules proxy counters'), + value: '', + }, + { + state: !data.rules_other_mark_exist ? 'success' : 'warning', + key: !data.rules_other_mark_exist + ? _('No other marking rules found') + : _('Additional marking rules found'), + value: '', + }, + ], + }); + + if (!atLeastOneGood) { + throw new Error('Nftables checks failed'); + } +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts new file mode 100644 index 0000000..15e0bc9 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts @@ -0,0 +1,105 @@ +import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; +import { PodkopShellMethods } from '../../../methods'; +import { updateCheckStore } from './updateCheckStore'; + +export async function runSingBoxCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; + + updateCheckStore({ + order, + code, + title, + description: _('Checking sing-box, please wait'), + state: 'loading', + items: [], + }); + + const singBoxChecks = await PodkopShellMethods.checkSingBox(); + + if (!singBoxChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _('Cannot receive Sing-box checks result'), + state: 'error', + items: [], + }); + + throw new Error('Sing-box checks failed'); + } + + const data = singBoxChecks.data; + + const allGood = + Boolean(data.sing_box_installed) && + Boolean(data.sing_box_version_ok) && + Boolean(data.sing_box_service_exist) && + Boolean(data.sing_box_autostart_disabled) && + Boolean(data.sing_box_process_running) && + Boolean(data.sing_box_ports_listening); + + const atLeastOneGood = + Boolean(data.sing_box_installed) || + Boolean(data.sing_box_version_ok) || + Boolean(data.sing_box_service_exist) || + Boolean(data.sing_box_autostart_disabled) || + Boolean(data.sing_box_process_running) || + Boolean(data.sing_box_ports_listening); + + function getStatus() { + if (allGood) { + return 'success'; + } + + if (atLeastOneGood) { + return 'warning'; + } + + return 'error'; + } + + updateCheckStore({ + order, + code, + title, + description: _('Sing-box checks passed'), + state: getStatus(), + items: [ + { + state: data.sing_box_installed ? 'success' : 'error', + key: _('Sing-box installed'), + value: '', + }, + { + state: data.sing_box_version_ok ? 'success' : 'error', + key: _('Sing-box version >= 1.12.4'), + value: '', + }, + { + state: data.sing_box_service_exist ? 'success' : 'error', + key: _('Sing-box service exist'), + value: '', + }, + { + state: data.sing_box_autostart_disabled ? 'success' : 'error', + key: _('Sing-box autostart disabled'), + value: '', + }, + { + state: data.sing_box_process_running ? 'success' : 'error', + key: _('Sing-box process running'), + value: '', + }, + { + state: data.sing_box_ports_listening ? 'success' : 'error', + key: _('Sing-box listening ports'), + value: '', + }, + ], + }); + + if (!atLeastOneGood || !data.sing_box_process_running) { + throw new Error('Sing-box checks failed'); + } +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/updateCheckStore.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/updateCheckStore.ts new file mode 100644 index 0000000..85583b8 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/checks/updateCheckStore.ts @@ -0,0 +1,20 @@ +import { IDiagnosticsChecksStoreItem, store } from '../../../services'; + +export function updateCheckStore( + check: IDiagnosticsChecksStoreItem, + minified?: boolean, +) { + const diagnosticsChecks = store.get().diagnosticsChecks; + const other = diagnosticsChecks.filter((item) => item.code !== check.code); + + const smallCheck: IDiagnosticsChecksStoreItem = { + ...check, + items: check.items.filter((item) => item.state !== 'success'), + }; + + const targetCheck = minified ? smallCheck : check; + + store.set({ + diagnosticsChecks: [...other, targetCheck], + }); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts new file mode 100644 index 0000000..4e91415 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts @@ -0,0 +1,124 @@ +import { + DIAGNOSTICS_CHECKS, + DIAGNOSTICS_CHECKS_MAP, +} from './checks/contstants'; +import { StoreType } from '../../services'; + +export const initialDiagnosticStore: Pick< + StoreType, + | 'diagnosticsChecks' + | 'diagnosticsRunAction' + | 'diagnosticsActions' + | 'diagnosticsSystemInfo' +> = { + diagnosticsSystemInfo: { + loading: true, + podkop_version: 'loading', + podkop_latest_version: 'loading', + luci_app_version: 'loading', + sing_box_version: 'loading', + openwrt_version: 'loading', + device_model: 'loading', + }, + diagnosticsActions: { + restart: { + loading: false, + }, + start: { + loading: false, + }, + stop: { + loading: false, + }, + enable: { + loading: false, + }, + disable: { + loading: false, + }, + globalCheck: { + loading: false, + }, + viewLogs: { + loading: false, + }, + showSingBoxConfig: { + loading: false, + }, + }, + diagnosticsRunAction: { loading: false }, + diagnosticsChecks: [ + { + code: DIAGNOSTICS_CHECKS.DNS, + title: DIAGNOSTICS_CHECKS_MAP.DNS.title, + order: DIAGNOSTICS_CHECKS_MAP.DNS.order, + description: _('Not running'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.SINGBOX, + title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title, + order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order, + description: _('Not running'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.NFT, + title: DIAGNOSTICS_CHECKS_MAP.NFT.title, + order: DIAGNOSTICS_CHECKS_MAP.NFT.order, + description: _('Not running'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.FAKEIP, + title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title, + order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order, + description: _('Not running'), + items: [], + state: 'skipped', + }, + ], +}; + +export const loadingDiagnosticsChecksStore: Pick< + StoreType, + 'diagnosticsChecks' +> = { + diagnosticsChecks: [ + { + code: DIAGNOSTICS_CHECKS.DNS, + title: DIAGNOSTICS_CHECKS_MAP.DNS.title, + order: DIAGNOSTICS_CHECKS_MAP.DNS.order, + description: _('Queued'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.SINGBOX, + title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title, + order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order, + description: _('Queued'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.NFT, + title: DIAGNOSTICS_CHECKS_MAP.NFT.title, + order: DIAGNOSTICS_CHECKS_MAP.NFT.order, + description: _('Queued'), + items: [], + state: 'skipped', + }, + { + code: DIAGNOSTICS_CHECKS.FAKEIP, + title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title, + order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order, + description: _('Queued'), + items: [], + state: 'skipped', + }, + ], +}; diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/index.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/index.ts new file mode 100644 index 0000000..2275b00 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/index.ts @@ -0,0 +1,9 @@ +import { render } from './renderDiagnostic'; +import { initController } from './initController'; +import { styles } from './styles'; + +export const DiagnosticTab = { + render, + initController, + styles, +}; diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts new file mode 100644 index 0000000..400dff9 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts @@ -0,0 +1,559 @@ +import { onMount, preserveScrollForPage } from '../../../helpers'; +import { runDnsCheck } from './checks/runDnsCheck'; +import { runSingBoxCheck } from './checks/runSingBoxCheck'; +import { runNftCheck } from './checks/runNftCheck'; +import { runFakeIPCheck } from './checks/runFakeIPCheck'; +import { loadingDiagnosticsChecksStore } from './diagnostic.store'; +import { logger, store, StoreType } from '../../services'; +import { + IRenderSystemInfoRow, + renderAvailableActions, + renderCheckSection, + renderRunAction, + renderSystemInfo, +} from './partials'; +import { PodkopShellMethods } from '../../methods'; +import { fetchServicesInfo } from '../../fetchers'; +import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion'; +import { renderModal } from '../../../partials'; + +async function fetchSystemInfo() { + const systemInfo = await PodkopShellMethods.getSystemInfo(); + + if (systemInfo.success) { + store.set({ + diagnosticsSystemInfo: { + loading: false, + ...systemInfo.data, + }, + }); + } else { + store.set({ + diagnosticsSystemInfo: { + loading: false, + podkop_version: _('unknown'), + podkop_latest_version: _('unknown'), + luci_app_version: _('unknown'), + sing_box_version: _('unknown'), + openwrt_version: _('unknown'), + device_model: _('unknown'), + }, + }); + } +} + +function renderDiagnosticsChecks() { + logger.debug('[DIAGNOSTIC]', 'renderDiagnosticsChecks'); + const diagnosticsChecks = store + .get() + .diagnosticsChecks.sort((a, b) => a.order - b.order); + const container = document.getElementById('pdk_diagnostic-page-checks'); + + const renderedDiagnosticsChecks = diagnosticsChecks.map((check) => + renderCheckSection(check), + ); + + return preserveScrollForPage(() => { + container!.replaceChildren(...renderedDiagnosticsChecks); + }); +} + +function renderDiagnosticRunActionWidget() { + logger.debug('[DIAGNOSTIC]', 'renderDiagnosticRunActionWidget'); + + const { loading } = store.get().diagnosticsRunAction; + const container = document.getElementById('pdk_diagnostic-page-run-check'); + + const renderedAction = renderRunAction({ + loading, + click: () => runChecks(), + }); + + return preserveScrollForPage(() => { + container!.replaceChildren(renderedAction); + }); +} + +async function handleRestart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.restart(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleRestart - e', e); + } finally { + setTimeout(async () => { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: false }, + }, + }); + store.reset(['diagnosticsChecks']); + }, 5000); + } +} + +async function handleStop() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.stop(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleStop - e', e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: false }, + }, + }); + store.reset(['diagnosticsChecks']); + } +} + +async function handleStart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.start(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleStart - e', e); + } finally { + setTimeout(async () => { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: false }, + }, + }); + store.reset(['diagnosticsChecks']); + }, 5000); + } +} + +async function handleEnable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.enable(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleEnable - e', e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: false }, + }, + }); + } +} + +async function handleDisable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: true }, + }, + }); + + try { + await PodkopShellMethods.disable(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleDisable - e', e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: false }, + }, + }); + } +} + +async function handleShowGlobalCheck() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + globalCheck: { loading: true }, + }, + }); + + try { + const globalCheck = await PodkopShellMethods.globalCheck(); + + if (globalCheck.success) { + ui.showModal( + _('Global check'), + renderModal(globalCheck.data as string, 'global_check'), + ); + } + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + globalCheck: { loading: false }, + }, + }); + } +} + +async function handleViewLogs() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + viewLogs: { loading: true }, + }, + }); + + try { + const viewLogs = await PodkopShellMethods.checkLogs(); + + if (viewLogs.success) { + ui.showModal( + _('View logs'), + renderModal(viewLogs.data as string, 'view_logs'), + ); + } + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + viewLogs: { loading: false }, + }, + }); + } +} + +async function handleShowSingBoxConfig() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + showSingBoxConfig: { loading: true }, + }, + }); + + try { + const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); + + if (showSingBoxConfig.success) { + ui.showModal( + _('Show sing-box config'), + renderModal(showSingBoxConfig.data as string, 'show_sing_box_config'), + ); + } + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleShowSingBoxConfig - e', e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + showSingBoxConfig: { loading: false }, + }, + }); + } +} + +function renderDiagnosticAvailableActionsWidget() { + const diagnosticsActions = store.get().diagnosticsActions; + const servicesInfoWidget = store.get().servicesInfoWidget; + logger.debug('[DIAGNOSTIC]', 'renderDiagnosticAvailableActionsWidget'); + + const podkopEnabled = Boolean(servicesInfoWidget.data.podkop); + const singBoxRunning = Boolean(servicesInfoWidget.data.singbox); + const atLeastOneServiceCommandLoading = + servicesInfoWidget.loading || + diagnosticsActions.restart.loading || + diagnosticsActions.start.loading || + diagnosticsActions.stop.loading; + + const container = document.getElementById('pdk_diagnostic-page-actions'); + + const renderedActions = renderAvailableActions({ + restart: { + loading: diagnosticsActions.restart.loading, + visible: true, + onClick: handleRestart, + disabled: atLeastOneServiceCommandLoading, + }, + start: { + loading: diagnosticsActions.start.loading, + visible: !singBoxRunning, + onClick: handleStart, + disabled: atLeastOneServiceCommandLoading, + }, + stop: { + loading: diagnosticsActions.stop.loading, + visible: singBoxRunning, + onClick: handleStop, + disabled: atLeastOneServiceCommandLoading, + }, + enable: { + loading: diagnosticsActions.enable.loading, + visible: !podkopEnabled, + onClick: handleEnable, + disabled: atLeastOneServiceCommandLoading, + }, + disable: { + loading: diagnosticsActions.disable.loading, + visible: podkopEnabled, + onClick: handleDisable, + disabled: atLeastOneServiceCommandLoading, + }, + globalCheck: { + loading: diagnosticsActions.globalCheck.loading, + visible: true, + onClick: handleShowGlobalCheck, + disabled: atLeastOneServiceCommandLoading, + }, + viewLogs: { + loading: diagnosticsActions.viewLogs.loading, + visible: true, + onClick: handleViewLogs, + disabled: atLeastOneServiceCommandLoading, + }, + showSingBoxConfig: { + loading: diagnosticsActions.showSingBoxConfig.loading, + visible: true, + onClick: handleShowSingBoxConfig, + disabled: atLeastOneServiceCommandLoading, + }, + }); + + return preserveScrollForPage(() => { + container!.replaceChildren(renderedActions); + }); +} + +function renderDiagnosticSystemInfoWidget() { + logger.debug('[DIAGNOSTIC]', 'renderDiagnosticSystemInfoWidget'); + const diagnosticsSystemInfo = store.get().diagnosticsSystemInfo; + + const container = document.getElementById('pdk_diagnostic-page-system-info'); + + function getPodkopVersionRow(): IRenderSystemInfoRow { + const loading = diagnosticsSystemInfo.loading; + const unknown = diagnosticsSystemInfo.podkop_version === _('unknown'); + const hasActualVersion = Boolean( + diagnosticsSystemInfo.podkop_latest_version, + ); + const version = normalizeCompiledVersion( + diagnosticsSystemInfo.podkop_version, + ); + const isDevVersion = version === 'dev'; + + if (loading || unknown || !hasActualVersion || isDevVersion) { + return { + key: 'Podkop', + value: version, + }; + } + + if (version !== diagnosticsSystemInfo.podkop_latest_version) { + return { + key: 'Podkop', + value: version, + tag: { + label: _('Outdated'), + kind: 'warning', + }, + }; + } + + return { + key: 'Podkop', + value: version, + tag: { + label: _('Latest'), + kind: 'success', + }, + }; + } + + const renderedSystemInfo = renderSystemInfo({ + items: [ + getPodkopVersionRow(), + { + key: 'Luci App', + value: normalizeCompiledVersion(diagnosticsSystemInfo.luci_app_version), + }, + { + key: 'Sing-box', + value: diagnosticsSystemInfo.sing_box_version, + }, + { + key: 'OS', + value: diagnosticsSystemInfo.openwrt_version, + }, + { + key: 'Device', + value: diagnosticsSystemInfo.device_model, + }, + ], + }); + + return preserveScrollForPage(() => { + container!.replaceChildren(renderedSystemInfo); + }); +} + +async function onStoreUpdate( + next: StoreType, + prev: StoreType, + diff: Partial, +) { + if (diff.diagnosticsChecks) { + renderDiagnosticsChecks(); + } + + if (diff.diagnosticsRunAction) { + renderDiagnosticRunActionWidget(); + } + + if (diff.diagnosticsActions || diff.servicesInfoWidget) { + renderDiagnosticAvailableActionsWidget(); + } + + if (diff.diagnosticsSystemInfo) { + renderDiagnosticSystemInfoWidget(); + } +} + +async function runChecks() { + try { + store.set({ + diagnosticsRunAction: { loading: true }, + diagnosticsChecks: loadingDiagnosticsChecksStore.diagnosticsChecks, + }); + + await runDnsCheck(); + + await runSingBoxCheck(); + + await runNftCheck(); + + await runFakeIPCheck(); + } catch (e) { + logger.error('[DIAGNOSTIC]', 'runChecks - e', e); + } finally { + store.set({ diagnosticsRunAction: { loading: false } }); + } +} + +function onPageMount() { + // Cleanup before mount + onPageUnmount(); + + // Add new listener + store.subscribe(onStoreUpdate); + + // Initial checks render + renderDiagnosticsChecks(); + + // Initial run checks action render + renderDiagnosticRunActionWidget(); + + // Initial available actions render + renderDiagnosticAvailableActionsWidget(); + + // Initial system info render + renderDiagnosticSystemInfoWidget(); + + // Initial services info fetch + fetchServicesInfo(); + + // Initial system info fetch + fetchSystemInfo(); +} + +function onPageUnmount() { + // Remove old listener + store.unsubscribe(onStoreUpdate); + + // Clear store + store.reset([ + 'diagnosticsActions', + 'diagnosticsSystemInfo', + 'diagnosticsChecks', + 'diagnosticsRunAction', + ]); +} + +function registerLifecycleListeners() { + store.subscribe((next, prev, diff) => { + if ( + diff.tabService && + next.tabService.current !== prev.tabService.current + ) { + logger.debug( + '[DIAGNOSTIC]', + 'active tab diff event, active tab:', + diff.tabService.current, + ); + const isDIAGNOSTICVisible = next.tabService.current === 'diagnostic'; + + if (isDIAGNOSTICVisible) { + logger.debug( + '[DIAGNOSTIC]', + 'registerLifecycleListeners', + 'onPageMount', + ); + return onPageMount(); + } + + if (!isDIAGNOSTICVisible) { + logger.debug( + '[DIAGNOSTIC]', + 'registerLifecycleListeners', + 'onPageUnmount', + ); + return onPageUnmount(); + } + } + }); +} + +export async function initController(): Promise { + onMount('diagnostic-status').then(() => { + logger.debug('[DIAGNOSTIC]', 'initController', 'onMount'); + onPageMount(); + registerLifecycleListeners(); + }); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/index.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/index.ts new file mode 100644 index 0000000..517093a --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/index.ts @@ -0,0 +1,4 @@ +export * from './renderAvailableActions'; +export * from './renderCheckSection'; +export * from './renderRunAction'; +export * from './renderSystemInfo'; diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts new file mode 100644 index 0000000..4b81bef --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts @@ -0,0 +1,122 @@ +import { renderButton } from '../../../../partials'; +import { + renderCircleCheckBigIcon24, + renderCirclePlayIcon24, + renderCircleStopIcon24, + renderCogIcon24, + renderPauseIcon24, + renderPlayIcon24, + renderRotateCcwIcon24, + renderSquareChartGanttIcon24, +} from '../../../../icons'; +import { insertIf } from '../../../../helpers'; + +interface ActionProps { + loading: boolean; + visible: boolean; + disabled: boolean; + onClick: () => void; +} + +interface IRenderAvailableActionsProps { + restart: ActionProps; + start: ActionProps; + stop: ActionProps; + enable: ActionProps; + disable: ActionProps; + globalCheck: ActionProps; + viewLogs: ActionProps; + showSingBoxConfig: ActionProps; +} + +export function renderAvailableActions({ + restart, + start, + stop, + enable, + disable, + globalCheck, + viewLogs, + showSingBoxConfig, +}: IRenderAvailableActionsProps) { + return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [ + E('b', {}, 'Available actions'), + ...insertIf(restart.visible, [ + renderButton({ + classNames: ['cbi-button-apply'], + onClick: restart.onClick, + icon: renderRotateCcwIcon24, + text: _('Restart podkop'), + loading: restart.loading, + disabled: restart.disabled, + }), + ]), + ...insertIf(stop.visible, [ + renderButton({ + classNames: ['cbi-button-remove'], + onClick: stop.onClick, + icon: renderCircleStopIcon24, + text: _('Stop podkop'), + loading: stop.loading, + disabled: stop.disabled, + }), + ]), + ...insertIf(start.visible, [ + renderButton({ + classNames: ['cbi-button-save'], + onClick: start.onClick, + icon: renderCirclePlayIcon24, + text: _('Start podkop'), + loading: start.loading, + disabled: start.disabled, + }), + ]), + ...insertIf(disable.visible, [ + renderButton({ + classNames: ['cbi-button-remove'], + onClick: disable.onClick, + icon: renderPauseIcon24, + text: _('Disable autostart'), + loading: disable.loading, + disabled: disable.disabled, + }), + ]), + ...insertIf(enable.visible, [ + renderButton({ + classNames: ['cbi-button-save'], + onClick: enable.onClick, + icon: renderPlayIcon24, + text: _('Enable autostart'), + loading: enable.loading, + disabled: enable.disabled, + }), + ]), + ...insertIf(globalCheck.visible, [ + renderButton({ + onClick: globalCheck.onClick, + icon: renderCircleCheckBigIcon24, + text: _('Get global check'), + loading: globalCheck.loading, + disabled: globalCheck.disabled, + }), + ]), + ...insertIf(viewLogs.visible, [ + renderButton({ + onClick: viewLogs.onClick, + icon: renderSquareChartGanttIcon24, + text: _('View logs'), + loading: viewLogs.loading, + disabled: viewLogs.disabled, + }), + ]), + ...insertIf(showSingBoxConfig.visible, [ + renderButton({ + onClick: showSingBoxConfig.onClick, + icon: renderCogIcon24, + text: _('Show sing-box config'), + loading: showSingBoxConfig.loading, + disabled: showSingBoxConfig.disabled, + }), + ]), + ]); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderCheckSection.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderCheckSection.ts new file mode 100644 index 0000000..a54da42 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderCheckSection.ts @@ -0,0 +1,190 @@ +import { + renderCheckIcon24, + renderCircleAlertIcon24, + renderCircleCheckIcon24, + renderCircleSlashIcon24, + renderCircleXIcon24, + renderLoaderCircleIcon24, + renderTriangleAlertIcon24, + renderXIcon24, +} from '../../../../icons'; +import { IDiagnosticsChecksStoreItem } from '../../../services'; + +type IRenderCheckSectionProps = IDiagnosticsChecksStoreItem; + +function renderCheckSummary(items: IRenderCheckSectionProps['items']) { + if (!items.length) { + return E('div', {}, ''); + } + + const renderedItems = items.map((item) => { + function getIcon() { + const iconWrap = E('span', { + class: 'pdk_diagnostic_alert__summary__item__icon', + }); + + if (item.state === 'success') { + iconWrap.appendChild(renderCheckIcon24()); + } + + if (item.state === 'warning') { + iconWrap.appendChild(renderTriangleAlertIcon24()); + } + + if (item.state === 'error') { + iconWrap.appendChild(renderXIcon24()); + } + + return iconWrap; + } + + return E( + 'div', + { + class: `pdk_diagnostic_alert__summary__item pdk_diagnostic_alert__summary__item--${item.state}`, + }, + [getIcon(), E('b', {}, item.key), E('div', {}, item.value)], + ); + }); + + return E('div', { class: 'pdk_diagnostic_alert__summary' }, renderedItems); +} + +function renderLoadingState(props: IRenderCheckSectionProps) { + const iconWrap = E('span', { class: 'pdk_diagnostic_alert__icon' }); + iconWrap.appendChild(renderLoaderCircleIcon24()); + + return E( + 'div', + { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--loading' }, + [ + iconWrap, + E('div', { class: 'pdk_diagnostic_alert__content' }, [ + E('b', { class: 'pdk_diagnostic_alert__title' }, props.title), + E( + 'div', + { class: 'pdk_diagnostic_alert__description' }, + props.description, + ), + ]), + E('div', {}, ''), + renderCheckSummary(props.items), + ], + ); +} + +function renderWarningState(props: IRenderCheckSectionProps) { + const iconWrap = E('span', { class: 'pdk_diagnostic_alert__icon' }); + iconWrap.appendChild(renderCircleAlertIcon24()); + + return E( + 'div', + { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--warning' }, + [ + iconWrap, + E('div', { class: 'pdk_diagnostic_alert__content' }, [ + E('b', { class: 'pdk_diagnostic_alert__title' }, props.title), + E( + 'div', + { class: 'pdk_diagnostic_alert__description' }, + props.description, + ), + ]), + E('div', {}, ''), + renderCheckSummary(props.items), + ], + ); +} + +function renderErrorState(props: IRenderCheckSectionProps) { + const iconWrap = E('span', { class: 'pdk_diagnostic_alert__icon' }); + iconWrap.appendChild(renderCircleXIcon24()); + + return E( + 'div', + { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--error' }, + [ + iconWrap, + E('div', { class: 'pdk_diagnostic_alert__content' }, [ + E('b', { class: 'pdk_diagnostic_alert__title' }, props.title), + E( + 'div', + { class: 'pdk_diagnostic_alert__description' }, + props.description, + ), + ]), + E('div', {}, ''), + renderCheckSummary(props.items), + ], + ); +} + +function renderSuccessState(props: IRenderCheckSectionProps) { + const iconWrap = E('span', { class: 'pdk_diagnostic_alert__icon' }); + iconWrap.appendChild(renderCircleCheckIcon24()); + + return E( + 'div', + { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--success' }, + [ + iconWrap, + E('div', { class: 'pdk_diagnostic_alert__content' }, [ + E('b', { class: 'pdk_diagnostic_alert__title' }, props.title), + E( + 'div', + { class: 'pdk_diagnostic_alert__description' }, + props.description, + ), + ]), + E('div', {}, ''), + renderCheckSummary(props.items), + ], + ); +} + +function renderSkippedState(props: IRenderCheckSectionProps) { + const iconWrap = E('span', { class: 'pdk_diagnostic_alert__icon' }); + iconWrap.appendChild(renderCircleSlashIcon24()); + + return E( + 'div', + { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--skipped' }, + [ + iconWrap, + E('div', { class: 'pdk_diagnostic_alert__content' }, [ + E('b', { class: 'pdk_diagnostic_alert__title' }, props.title), + E( + 'div', + { class: 'pdk_diagnostic_alert__description' }, + props.description, + ), + ]), + E('div', {}, ''), + renderCheckSummary(props.items), + ], + ); +} + +export function renderCheckSection(props: IRenderCheckSectionProps) { + if (props.state === 'loading') { + return renderLoadingState(props); + } + + if (props.state === 'warning') { + return renderWarningState(props); + } + + if (props.state === 'error') { + return renderErrorState(props); + } + + if (props.state === 'success') { + return renderSuccessState(props); + } + + if (props.state === 'skipped') { + return renderSkippedState(props); + } + + return E('div', {}, _('Not implement yet')); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts new file mode 100644 index 0000000..24c578c --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts @@ -0,0 +1,22 @@ +import { renderButton } from '../../../../partials'; +import { renderSearchIcon24 } from '../../../../icons'; + +interface IRenderDiagnosticRunActionProps { + loading: boolean; + click: () => void; +} + +export function renderRunAction({ + loading, + click, +}: IRenderDiagnosticRunActionProps) { + return E('div', { class: 'pdk_diagnostic-page__run_check_wrapper' }, [ + renderButton({ + text: _('Run Diagnostic'), + onClick: click, + icon: renderSearchIcon24, + loading, + classNames: ['cbi-button-apply'], + }), + ]); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts new file mode 100644 index 0000000..4b05a45 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts @@ -0,0 +1,49 @@ +import { insertIf } from '../../../../helpers'; + +export interface IRenderSystemInfoRow { + key: string; + value: string; + tag?: { + label: string; + kind: 'warning' | 'success'; + }; +} + +interface IRenderSystemInfoProps { + items: Array; +} + +export function renderSystemInfo({ items }: IRenderSystemInfoProps) { + return E('div', { class: 'pdk_diagnostic-page__right-bar__system-info' }, [ + E( + 'b', + { class: 'pdk_diagnostic-page__right-bar__system-info__title' }, + 'System information', + ), + ...items.map((item) => { + const tagClass = [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag', + ...insertIf(item.tag?.kind === 'warning', [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag--warning', + ]), + ...insertIf(item.tag?.kind === 'success', [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag--success', + ]), + ] + .filter(Boolean) + .join(' '); + + return E( + 'div', + { class: 'pdk_diagnostic-page__right-bar__system-info__row' }, + [ + E('b', {}, item.key), + E('div', {}, [ + E('span', {}, item.value), + E('span', { class: tagClass }, item?.tag?.label), + ]), + ], + ); + }), + ]); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/renderDiagnostic.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/renderDiagnostic.ts new file mode 100644 index 0000000..2b98d67 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/renderDiagnostic.ts @@ -0,0 +1,15 @@ +export function render() { + return E('div', { id: 'diagnostic-status', class: 'pdk_diagnostic-page' }, [ + E('div', { class: 'pdk_diagnostic-page__left-bar' }, [ + E('div', { id: 'pdk_diagnostic-page-run-check' }), + E('div', { + class: 'pdk_diagnostic-page__checks', + id: 'pdk_diagnostic-page-checks', + }), + ]), + E('div', { class: 'pdk_diagnostic-page__right-bar' }, [ + E('div', { id: 'pdk_diagnostic-page-actions' }), + E('div', { id: 'pdk_diagnostic-page-system-info' }), + ]), + ]); +} diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts new file mode 100644 index 0000000..75a9042 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts @@ -0,0 +1,165 @@ +// language=CSS +export const styles = ` + +#cbi-podkop-diagnostic-_mount_node > div { + width: 100%; +} + +#cbi-podkop-diagnostic > h3 { + display: none; +} + +.pdk_diagnostic-page { + display: grid; + grid-template-columns: 2fr 1fr; + grid-column-gap: 10px; + align-items: start; +} + +@media (max-width: 800px) { + .pdk_diagnostic-page { + grid-template-columns: 1fr; + } +} + +.pdk_diagnostic-page__right-bar { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__right-bar__actions { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + + display: grid; + grid-template-columns: auto; + grid-row-gap: 10px; + +} + +.pdk_diagnostic-page__right-bar__system-info { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + + display: grid; + grid-template-columns: auto; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__right-bar__system-info__title { + +} + +.pdk_diagnostic-page__right-bar__system-info__row { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 5px; +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag { + padding: 2px 4px; + border: 1px transparent solid; + border-radius: 4px; + margin-left: 5px; +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag--warning { + border: 1px var(--warn-color-medium, orange) solid; + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag--success { + border: 1px var(--success-color-medium, green) solid; + color: var(--success-color-medium, green); +} + +.pdk_diagnostic-page__left-bar { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__run_check_wrapper {} + +.pdk_diagnostic-page__run_check_wrapper button { + width: 100%; +} + +.pdk_diagnostic-page__checks { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic_alert { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + + display: grid; + grid-template-columns: 24px 1fr; + grid-column-gap: 10px; + align-items: center; + padding: 10px; +} + +.pdk_diagnostic_alert--loading { + border: 2px var(--primary-color-high, dodgerblue) solid; +} + +.pdk_diagnostic_alert--warning { + border: 2px var(--warn-color-medium, orange) solid; + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic_alert--error { + border: 2px var(--error-color-medium, red) solid; + color: var(--error-color-medium, red); +} + +.pdk_diagnostic_alert--success { + border: 2px var(--success-color-medium, green) solid; + color: var(--success-color-medium, green); +} + +.pdk_diagnostic_alert--skipped {} + +.pdk_diagnostic_alert__icon {} + +.pdk_diagnostic_alert__content {} + +.pdk_diagnostic_alert__title { + display: block; +} + +.pdk_diagnostic_alert__description {} + +.pdk_diagnostic_alert__summary { + margin-top: 10px; +} + +.pdk_diagnostic_alert__summary__item { + display: grid; + grid-template-columns: 16px auto 1fr; + grid-column-gap: 10px; +} + +.pdk_diagnostic_alert__summary__item--error { + color: var(--error-color-medium, red); +} + +.pdk_diagnostic_alert__summary__item--warning { + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic_alert__summary__item--success { + color: var(--success-color-medium, green); +} + +.pdk_diagnostic_alert__summary__item__icon { + width: 16px; + height: 16px; +} +`; diff --git a/fe-app-podkop/src/podkop/tabs/index.ts b/fe-app-podkop/src/podkop/tabs/index.ts index b58b6c9..b49ac00 100644 --- a/fe-app-podkop/src/podkop/tabs/index.ts +++ b/fe-app-podkop/src/podkop/tabs/index.ts @@ -1 +1,2 @@ export * from './dashboard'; +export * from './diagnostic'; diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts index 531f648..9b8aefd 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-podkop/src/podkop/types.ts @@ -1,5 +1,79 @@ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace ClashAPI { + export interface ProxyHistoryEntry { + time: string; + delay: number; + } + + export interface ProxyBase { + type: string; + name: string; + udp: boolean; + history: ProxyHistoryEntry[]; + now?: string; + all?: string[]; + } + + export interface Proxies { + proxies: Record; + } +} + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Podkop { + // Available commands: + // start Start podkop service + // stop Stop podkop service + // reload Reload podkop configuration + // restart Restart podkop service + // enable Enable podkop autostart + // disable Disable podkop autostart + // main Run main podkop process + // list_update Update domain lists + // check_proxy Check proxy connectivity + // check_nft Check NFT rules + // check_nft_rules Check NFT rules status + // check_sing_box Check sing-box installation and status + // check_logs Show podkop logs from system journal + // check_sing_box_logs Show sing-box logs + // check_fakeip Test FakeIP on router + // clash_api Clash API interface for managing proxies and groups + // show_config Display current podkop configuration + // show_version Show podkop version + // show_sing_box_config Show sing-box configuration + // show_sing_box_version Show sing-box version + // show_system_info Show system information + // get_status Get podkop service status + // get_sing_box_status Get sing-box service status + // check_dns_available Check DNS server availability + // global_check Run global system check + + export enum AvailableMethods { + CHECK_DNS_AVAILABLE = 'check_dns_available', + CHECK_FAKEIP = 'check_fakeip', + CHECK_NFT_RULES = 'check_nft_rules', + GET_STATUS = 'get_status', + CHECK_SING_BOX = 'check_sing_box', + GET_SING_BOX_STATUS = 'get_sing_box_status', + CLASH_API = 'clash_api', + RESTART = 'restart', + START = 'start', + STOP = 'stop', + ENABLE = 'enable', + DISABLE = 'disable', + GLOBAL_CHECK = 'global_check', + SHOW_SING_BOX_CONFIG = 'show_sing_box_config', + CHECK_LOGS = 'check_logs', + GET_SYSTEM_INFO = 'get_system_info', + } + + export enum AvailableClashAPIMethods { + GET_PROXIES = 'get_proxies', + GET_PROXY_LATENCY = 'get_proxy_latency', + GET_GROUP_LATENCY = 'get_group_latency', + SET_GROUP_PROXY = 'set_group_proxy', + } + export interface Outbound { code: string; displayName: string; @@ -16,30 +90,30 @@ export namespace Podkop { } export interface ConfigProxyUrlTestSection { - mode: 'proxy'; + connection_type: 'proxy'; proxy_config_type: 'urltest'; urltest_proxy_links: string[]; } export interface ConfigProxyUrlSection { - mode: 'proxy'; + connection_type: 'proxy'; proxy_config_type: 'url'; proxy_string: string; } export interface ConfigProxyOutboundSection { - mode: 'proxy'; + connection_type: 'proxy'; proxy_config_type: 'outbound'; outbound_json: string; } export interface ConfigVpnSection { - mode: 'vpn'; + connection_type: 'vpn'; interface: string; } export interface ConfigBlockSection { - mode: 'block'; + connection_type: 'block'; } export type ConfigBaseSection = @@ -51,6 +125,75 @@ export namespace Podkop { export type ConfigSection = ConfigBaseSection & { '.name': string; - '.type': 'main' | 'extra'; + '.type': 'settings' | 'section'; }; + + export interface MethodSuccessResponse { + success: true; + data: T; + } + + export interface MethodFailureResponse { + success: false; + error: string; + } + + export type MethodResponse = + | MethodSuccessResponse + | MethodFailureResponse; + + export interface DnsCheckResult { + dns_type: 'udp' | 'doh' | 'dot'; + dns_server: string; + dns_status: 0 | 1; + dns_on_router: 0 | 1; + bootstrap_dns_server: string; + bootstrap_dns_status: 0 | 1; + dhcp_config_status: 0 | 1; + } + + export interface NftRulesCheckResult { + table_exist: 0 | 1; + rules_mangle_exist: 0 | 1; + rules_mangle_counters: 0 | 1; + rules_mangle_output_exist: 0 | 1; + rules_mangle_output_counters: 0 | 1; + rules_proxy_exist: 0 | 1; + rules_proxy_counters: 0 | 1; + rules_other_mark_exist: 0 | 1; + } + + export interface SingBoxCheckResult { + sing_box_installed: 0 | 1; + sing_box_version_ok: 0 | 1; + sing_box_service_exist: 0 | 1; + sing_box_autostart_disabled: 0 | 1; + sing_box_process_running: 0 | 1; + sing_box_ports_listening: 0 | 1; + } + + export interface FakeIPCheckResult { + fakeip: boolean; + IP: string; + } + + export interface GetStatus { + enabled: number; + status: string; + } + + export interface GetSingBoxStatus { + running: number; + enabled: number; + status: string; + } + + export interface GetSystemInfo { + podkop_version: string; + podkop_latest_version: string; + luci_app_version: string; + sing_box_version: string; + openwrt_version: string; + device_model: string; + } } diff --git a/fe-app-podkop/src/styles.ts b/fe-app-podkop/src/styles.ts index b135ef5..e40000d 100644 --- a/fe-app-podkop/src/styles.ts +++ b/fe-app-podkop/src/styles.ts @@ -1,150 +1,45 @@ // language=CSS +import { DashboardTab, DiagnosticTab } from './podkop'; +import { PartialStyles } from './partials'; + export const GlobalStyles = ` -.cbi-value { - margin-bottom: 10px !important; -} +${DashboardTab.styles} +${DiagnosticTab.styles} +${PartialStyles} -#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); -} - -#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra { +/* Hide extra H3 for settings tab */ +#cbi-podkop-settings > h3 { display: none; } -#cbi-podkop-main-_status > div { - width: 100%; +/* Hide extra H3 for sections tab */ +#cbi-podkop-section > h3:nth-child(1) { + display: none; } -/* Dashboard styles */ - -.pdk_dashboard-page { - width: 100%; - --dashboard-grid-columns: 4; -} - -@media (max-width: 900px) { - .pdk_dashboard-page { - --dashboard-grid-columns: 2; - } -} - -.pdk_dashboard-page__widgets-section { - margin-top: 10px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; -} - -.pdk_dashboard-page__widgets-section__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; -} - -.pdk_dashboard-page__widgets-section__item__title {} - -.pdk_dashboard-page__widgets-section__item__row {} - -.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--error-color-medium, red); -} - -.pdk_dashboard-page__widgets-section__item__row__key {} - -.pdk_dashboard-page__widgets-section__item__row__value {} - -.pdk_dashboard-page__outbound-section { - margin-top: 10px; - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; -} - -.pdk_dashboard-page__outbound-section__title-section { - display: flex; - align-items: center; - justify-content: space-between; -} - -.pdk_dashboard-page__outbound-section__title-section__title { - color: var(--text-color-high); - font-weight: 700; -} - -.pdk_dashboard-page__outbound-grid { - margin-top: 5px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; -} - -.pdk_dashboard-page__outbound-grid__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - transition: border 0.2s ease; -} - -.pdk_dashboard-page__outbound-grid__item--selectable { - cursor: pointer; -} - -.pdk_dashboard-page__outbound-grid__item--selectable:hover { - border-color: var(--primary-color-high, dodgerblue); -} - -.pdk_dashboard-page__outbound-grid__item--active { - border-color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__outbound-grid__item__footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 10px; -} - -.pdk_dashboard-page__outbound-grid__item__type {} - -.pdk_dashboard-page__outbound-grid__item__latency--empty { - color: var(--primary-color-low, lightgray); -} - -.pdk_dashboard-page__outbound-grid__item__latency--green { - color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__outbound-grid__item__latency--yellow { - color: var(--warn-color-medium, orange); -} - -.pdk_dashboard-page__outbound-grid__item__latency--red { - color: var(--error-color-medium, red); +/* Vertical align for remove section action button */ +#cbi-podkop-section > .cbi-section-remove { + margin-bottom: -32px; } +/* Centered class helper */ .centered { display: flex; align-items: center; justify-content: center; } +/* Rotate class helper */ +.rotate { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + /* Skeleton styles*/ .skeleton { background-color: var(--background-color-low, #e0e0e0); @@ -174,4 +69,44 @@ export const GlobalStyles = ` left: 150%; } } +/* Toast */ +.toast-container { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + z-index: 9999; + font-family: system-ui, sans-serif; +} + +.toast { + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s ease, transform 0.3s ease; + padding: 10px 16px; + border-radius: 6px; + color: #fff; + font-size: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + min-width: 220px; + max-width: 340px; + text-align: center; +} + +.toast-success { + background-color: #28a745; +} + +.toast-error { + background-color: #dc3545; +} + +.toast.visible { + opacity: 1; + transform: translateY(0); +} `; diff --git a/fe-app-podkop/src/validators/index.ts b/fe-app-podkop/src/validators/index.ts index 88e6b03..9bdad27 100644 --- a/fe-app-podkop/src/validators/index.ts +++ b/fe-app-podkop/src/validators/index.ts @@ -10,3 +10,4 @@ export * from './validateVlessUrl'; export * from './validateOutboundJson'; export * from './validateTrojanUrl'; export * from './validateProxyUrl'; +export * from './validateSocksUrl'; diff --git a/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js b/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js new file mode 100644 index 0000000..e9c4da4 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { validateSocksUrl } from '../validateSocksUrl'; + +const validUrls = [ + ['socks4 basic', 'socks4://127.0.0.1:1080'], + ['socks4a basic', 'socks4a://127.0.0.1:1080'], + ['socks5 basic', 'socks5://127.0.0.1:1080'], + ['socks5 with username', 'socks5://user@127.0.0.1:1080'], + ['socks5 with username/password', 'socks5://user:pass@127.0.0.1:1080'], + ['socks5 with domain', 'socks5://user:pass@my.proxy.com:1080'], + ['socks5 with dash in domain', 'socks5://user:pass@fast-proxy.net:8080'], + ['socks5 with uppercase domain', 'socks5://USER:PASSWORD@Example.COM:1080'], +]; + +const invalidUrls = [ + ['no prefix', '127.0.0.1:1080'], + ['wrong prefix', 'http://127.0.0.1:1080'], + ['missing host', 'socks5://user:pass@:1080'], + ['missing port', 'socks5://127.0.0.1'], + ['invalid port (non-numeric)', 'socks5://127.0.0.1:abc'], + ['invalid port (too high)', 'socks5://127.0.0.1:99999'], + ['space in url', 'socks5://127.0. 0.1:1080'], + ['missing username when auth provided', 'socks5://:pass@127.0.0.1:1080'], + ['invalid domain chars', 'socks5://user:pass@exa_mple.com:1080'], + ['extra symbol', 'socks5:///127.0.0.1:1080'], +]; + +describe('validateSocksUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateSocksUrl(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateSocksUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range (0)', () => { + const res = validateSocksUrl('socks5://127.0.0.1:0'); + expect(res.valid).toBe(false); + }); + + it('detects invalid port range (65536)', () => { + const res = validateSocksUrl('socks5://127.0.0.1:65536'); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/validateProxyUrl.ts b/fe-app-podkop/src/validators/validateProxyUrl.ts index ec3fe47..15a3003 100644 --- a/fe-app-podkop/src/validators/validateProxyUrl.ts +++ b/fe-app-podkop/src/validators/validateProxyUrl.ts @@ -2,6 +2,7 @@ import { ValidationResult } from './types'; import { validateShadowsocksUrl } from './validateShadowsocksUrl'; import { validateVlessUrl } from './validateVlessUrl'; import { validateTrojanUrl } from './validateTrojanUrl'; +import { validateSocksUrl } from './validateSocksUrl'; // TODO refactor current validation and add tests export function validateProxyUrl(url: string): ValidationResult { @@ -17,8 +18,14 @@ export function validateProxyUrl(url: string): ValidationResult { return validateTrojanUrl(url); } + if (/^socks(4|4a|5):\/\//.test(url)) { + return validateSocksUrl(url); + } + return { valid: false, - message: _('URL must start with vless:// or ss:// or trojan://'), + message: _( + 'URL must start with vless://, ss://, trojan://, or socks4/5://', + ), }; } diff --git a/fe-app-podkop/src/validators/validateSocksUrl.ts b/fe-app-podkop/src/validators/validateSocksUrl.ts new file mode 100644 index 0000000..b59ae6c --- /dev/null +++ b/fe-app-podkop/src/validators/validateSocksUrl.ts @@ -0,0 +1,81 @@ +import { ValidationResult } from './types'; +import { validateDomain } from './validateDomain'; +import { validateIPV4 } from './validateIp'; + +export function validateSocksUrl(url: string): ValidationResult { + try { + if (!/^socks(4|4a|5):\/\//.test(url)) { + return { + valid: false, + message: _( + 'Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://', + ), + }; + } + + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _('Invalid SOCKS URL: must not contain spaces'), + }; + } + + const body = url.replace(/^socks(4|4a|5):\/\//, ''); + const [authAndHost] = body.split('#'); // отбрасываем hash, если есть + const [credentials, hostPortPart] = authAndHost.includes('@') + ? authAndHost.split('@') + : [null, authAndHost]; + + if (credentials) { + const [username, _password] = credentials.split(':'); + if (!username) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing username'), + }; + } + } + + if (!hostPortPart) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing host and port'), + }; + } + + const [host, port] = hostPortPart.split(':'); + + if (!host) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing hostname or IP'), + }; + } + + if (!port) { + return { valid: false, message: _('Invalid SOCKS URL: missing port') }; + } + + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _('Invalid SOCKS URL: invalid port number'), + }; + } + + const ipv4Result = validateIPV4(host); + const domainResult = validateDomain(host); + + if (!ipv4Result.valid && !domainResult.valid) { + return { + valid: false, + message: _('Invalid SOCKS URL: invalid host format'), + }; + } + } catch (_e) { + return { valid: false, message: _('Invalid SOCKS URL: parsing failed') }; + } + + return { valid: true, message: _('Valid') }; +} diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index 06a84ba..deea0a9 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -1,5 +1,5 @@ import { ValidationResult } from './types'; -import { parseQueryString } from '../helpers'; +import { parseQueryString } from '../helpers/parseQueryString'; export function validateVlessUrl(url: string): ValidationResult { try { @@ -96,6 +96,14 @@ export function validateVlessUrl(url: string): ValidationResult { }; } + if (params.flow === 'xtls-rprx-vision-udp443') { + return { + valid: false, + message: + 'Invalid VLESS URL: flow xtls-rprx-vision-udp443 is not supported', + }; + } + return { valid: true, message: _('Valid') }; } catch (_e) { return { valid: false, message: _('Invalid VLESS URL: parsing failed') }; diff --git a/fe-app-podkop/watch-upload.js b/fe-app-podkop/watch-upload.js index db0b1ea..0f57832 100644 --- a/fe-app-podkop/watch-upload.js +++ b/fe-app-podkop/watch-upload.js @@ -16,67 +16,85 @@ const config = { : { password: process.env.SFTP_PASS }), }; -const localDir = path.resolve(process.env.LOCAL_DIR || './dist'); -const remoteDir = process.env.REMOTE_DIR || '/www/luci-static/mypkg'; +const syncDirs = [ + { + local: path.resolve(process.env.LOCAL_DIR_FE ?? '../luci-app-podkop/htdocs/luci-static/resources/view/podkop'), + remote: process.env.REMOTE_DIR_FE ?? '/www/luci-static/resources/view/podkop', + }, + { + local: path.resolve(process.env.LOCAL_DIR_BIN ?? '../podkop/files/usr/bin/'), + remote: process.env.REMOTE_DIR_BIN ?? '/usr/bin/', + }, + { + local: path.resolve(process.env.LOCAL_DIR_LIB ?? '../podkop/files/usr/lib/'), + remote: process.env.REMOTE_DIR_LIB ?? '/usr/lib/podkop/', + }, + { + local: path.resolve(process.env.LOCAL_DIR_INIT ?? '../podkop/files/etc/init.d/'), + remote: process.env.REMOTE_DIR_INIT ?? '/etc/init.d/', + } +]; -async function uploadFile(filePath) { - const relativePath = path.relative(localDir, filePath); - const remotePath = path.posix.join(remoteDir, relativePath); +async function uploadFile(filePath, baseDir, remoteBase) { + const relativePath = path.relative(baseDir, filePath); + const remotePath = path.posix.join(remoteBase, relativePath); - console.log(`Uploading: ${relativePath} -> ${remotePath}`); + console.log(`↑ Uploading: ${relativePath} -> ${remotePath}`); try { await sftp.fastPut(filePath, remotePath); - console.log(`Uploaded: ${relativePath}`); + console.log(`✓ Uploaded: ${relativePath}`); } catch (err) { - console.error(`Failed: ${relativePath}: ${err.message}`); + console.error(`✗ Failed: ${relativePath}: ${err.message}`); } } -async function deleteFile(filePath) { - const relativePath = path.relative(localDir, filePath); - const remotePath = path.posix.join(remoteDir, relativePath); +async function deleteFile(filePath, baseDir, remoteBase) { + const relativePath = path.relative(baseDir, filePath); + const remotePath = path.posix.join(remoteBase, relativePath); - console.log(`Removing: ${relativePath}`); + console.log(`⨯ Removing: ${relativePath}`); try { await sftp.delete(remotePath); - console.log(`Removed: ${relativePath}`); + console.log(`✓ Removed: ${relativePath}`); } catch (err) { - console.warn(`Could not delete ${relativePath}: ${err.message}`); + console.warn(`⚠ Could not delete ${relativePath}: ${err.message}`); } } async function uploadAllFiles() { - console.log('Uploading all files from', localDir); - - const files = await glob(`${localDir}/**/*`, { nodir: true }); - for (const file of files) { - await uploadFile(file); + for (const { local, remote } of syncDirs) { + console.log(`📂 Uploading all from ${local}`); + const files = await glob(`${local}/**/*`, { nodir: true }); + for (const file of files) { + await uploadFile(file, local, remote); + } } - - console.log('Initial upload complete!'); + console.log('✅ Initial upload complete!'); } async function main() { await sftp.connect(config); - console.log(`Connected to ${config.host}`); + console.log(`🔌 Connected to ${config.host}`); await uploadAllFiles(); - chokidar - .watch(localDir, { ignoreInitial: true }) - .on('all', async (event, filePath) => { - if (event === 'add' || event === 'change') { - await uploadFile(filePath); - } else if (event === 'unlink') { - await deleteFile(filePath); - } - }); + for (const { local, remote } of syncDirs) { + chokidar.watch(local, { ignoreInitial: true }).on('all', async (event, filePath) => { + if (event === 'add' || event === 'change') { + await uploadFile(filePath, local, remote); + } else if (event === 'unlink') { + await deleteFile(filePath, local, remote); + } + }); + } process.on('SIGINT', async () => { - console.log('Disconnecting...'); + console.log('👋 Disconnecting...'); await sftp.end(); process.exit(); }); } -main().catch(console.error); +main().catch((err) => { + console.error('💥 Fatal:', err); +}); diff --git a/fe-app-podkop/yarn.lock b/fe-app-podkop/yarn.lock index 93738ea..a87787d 100644 --- a/fe-app-podkop/yarn.lock +++ b/fe-app-podkop/yarn.lock @@ -2,6 +2,78 @@ # yarn lockfile v1 +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/generator@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/parser@7.28.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== + dependencies: + "@babel/types" "^7.28.4" + +"@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" + integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + debug "^4.3.1" + +"@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@esbuild/aix-ppc64@0.25.10": version "0.25.10" resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97" @@ -245,7 +317,7 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@jridgewell/gen-mapping@^0.3.2": +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.2": version "0.3.13" resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== @@ -263,7 +335,7 @@ resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.24": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -996,7 +1068,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.3.2: +fast-glob@3.3.3, fast-glob@^3.3.2: version "3.3.3" resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -1215,6 +1287,11 @@ joycon@^3.1.1: resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-tokens@^9.0.1: version "9.0.1" resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" @@ -1227,6 +1304,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" diff --git a/install.sh b/install.sh index c84051b..3879878 100755 --- a/install.sh +++ b/install.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck shell=dash REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest" DOWNLOAD_DIR="/tmp/podkop" @@ -60,6 +61,45 @@ pkg_install() { fi } +update_config() { + printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия podkop. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить Podkop заново. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/podkop-070 ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: LINK ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" + printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" + + echo "" + + printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Detected old podkop version. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE podkop. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/podkop-070 ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: LINK ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" + printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" + + msg "Continue? (yes/no)" + + while true; do + read -r -p '' CONFIG_UPDATE + case $CONFIG_UPDATE in + + yes|y|Y) + mv /etc/config/podkop /etc/config/podkop-070 + wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop + msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070" + break + ;; + *) + msg "Exit" + exit 1 + ;; + esac + done +} + main() { check_system sing_box @@ -74,7 +114,7 @@ main() { msg "Installed podkop..." fi - if command -v curl &> /dev/null; then + if command -v curl >/dev/null 2>&1; then check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest") if echo "$check_response" | grep -q 'API rate limit '; then @@ -90,8 +130,7 @@ main() { grep_url_pattern='https://[^"[:space:]]*\.ipk' fi - download_success=0 - while read -r url; do + wget -qO- "$REPO" | grep -o "$grep_url_pattern" | while read -r url; do filename=$(basename "$url") filepath="$DOWNLOAD_DIR/$filename" @@ -101,7 +140,6 @@ main() { if wget -q -O "$filepath" "$url"; then if [ -s "$filepath" ]; then msg "$filename successfully downloaded" - download_success=1 break fi fi @@ -113,15 +151,22 @@ main() { if [ $attempt -eq $COUNT ]; then msg "Failed to download $filename after $COUNT attempts" fi - done < <(wget -qO- "$REPO" | grep -o "$grep_url_pattern") + done - if [ $download_success -eq 0 ]; then + # Check if any files were downloaded + if ! ls "$DOWNLOAD_DIR"/*podkop* >/dev/null 2>&1; then msg "No packages were downloaded successfully" exit 1 fi for pkg in podkop luci-app-podkop; do - file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1) + file="" + for f in "$DOWNLOAD_DIR"/"$pkg"*; do + if [ -f "$f" ]; then + file=$(basename "$f") + break + fi + done if [ -n "$file" ]; then msg "Installing $file" pkg_install "$DOWNLOAD_DIR/$file" @@ -129,7 +174,13 @@ main() { fi done - ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1) + ru="" + for f in "$DOWNLOAD_DIR"/luci-i18n-podkop-ru*; do + if [ -f "$f" ]; then + ru=$(basename "$f") + break + fi + done if [ -n "$ru" ]; then if pkg_is_installed luci-i18n-podkop-ru; then msg "Upgraded ru translation..." @@ -189,6 +240,35 @@ check_system() { exit 1 fi + # Check version + # if command -v podkop > /dev/null 2>&1; then + # local version + # version=$(/usr/bin/podkop show_version 2> /dev/null) + # if [ -n "$version" ]; then + # version=$(echo "$version" | sed 's/^v//') + # local major + # local minor + # local patch + # major=$(echo "$version" | cut -d. -f1) + # minor=$(echo "$version" | cut -d. -f2) + # patch=$(echo "$version" | cut -d. -f3) + + # # Compare version: must be >= 0.7.0 + # if [ "$major" -gt 0 ] || + # [ "$major" -eq 0 ] && [ "$minor" -gt 7 ] || + # [ "$major" -eq 0 ] && [ "$minor" -eq 7 ] && [ "$patch" -ge 0 ]; then + # msg "Podkop version >= 0.7.0" + # break + # else + # msg "Podkop version < 0.7.0" + # update_config + # fi + # else + # msg "Unknown podkop version" + # update_config + # fi + # fi + if pkg_is_installed https-dns-proxy; then msg "Сonflicting package detected: https-dns-proxy. Remove?" @@ -219,7 +299,7 @@ sing_box() { sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}') required_version="1.12.4" - if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then + if [ "$(printf '%s\n%s\n' "$sing_box_version" "$required_version" | sort -V | head -n 1)" != "$required_version" ]; then msg "sing-box version $sing_box_version is older than required $required_version" msg "Removing old version..." service podkop stop diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js deleted file mode 100644 index f06ae90..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js +++ /dev/null @@ -1,362 +0,0 @@ -'use strict'; -'require form'; -'require baseclass'; -'require tools.widgets as widgets'; -'require view.podkop.main as main'; - -function createAdditionalSection(mainSection) { - let o = mainSection.tab('additional', _('Additional Settings')); - - o = mainSection.taboption( - 'additional', - form.Flag, - 'yacd', - _('Yacd enable'), - `${main.getClashUIUrl()}`, - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'exclude_ntp', - _('Exclude NTP'), - _('Allows you to exclude NTP protocol traffic from the tunnel'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'quic_disable', - _('QUIC disable'), - _('For issues with the video stream'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'update_interval', - _('List Update Frequency'), - _('Select how often the lists will be updated'), - ); - Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '1d'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'dns_type', - _('DNS Protocol Type'), - _('Select DNS protocol to use'), - ); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Value, - 'dns_server', - _('DNS Server'), - _('Select or enter DNS server address'), - ); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'bootstrap_dns_server', - _('Bootstrap DNS server'), - _( - 'The DNS server used to look up the IP address of an upstream DNS server', - ), - ); - Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '77.88.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'dns_rewrite_ttl', - _('DNS Rewrite TTL'), - _('Time in seconds for DNS record caching (default: 60)'), - ); - o.default = '60'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('TTL value cannot be empty'); - } - - const ttl = parseInt(value); - if (isNaN(ttl) || ttl < 0) { - return _('TTL must be a positive number'); - } - - return true; - }; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'config_path', - _('Config File Path'), - _( - 'Select path for sing-box config file. Change this ONLY if you know what you are doing', - ), - ); - o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); - o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); - o.default = '/etc/sing-box/config.json'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Value, - 'cache_path', - _('Cache File Path'), - _( - 'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing', - ), - ); - o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); - o.value( - '/usr/share/sing-box/cache.db', - 'Flash (/usr/share/sing-box/cache.db)', - ); - o.default = '/tmp/sing-box/cache.db'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('Cache file path cannot be empty'); - } - - if (!value.startsWith('/')) { - return _('Path must be absolute (start with /)'); - } - - if (!value.endsWith('cache.db')) { - return _('Path must end with cache.db'); - } - - const parts = value.split('/').filter(Boolean); - if (parts.length < 2) { - return _('Path must contain at least one directory (like /tmp/cache.db)'); - } - - return true; - }; - - o = mainSection.taboption( - 'additional', - widgets.DeviceSelect, - 'iface', - _('Source Network Interface'), - _('Select the network interface from which the traffic will originate'), - ); - o.ucisection = 'main'; - o.default = 'br-lan'; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.multiple = true; - o.filter = function (section_id, value) { - // Block specific interface names from being selectable - const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; - if (blocked.includes(value)) { - return false; - } - - // Try to find the device object by its name - const device = this.devices.find((dev) => dev.getName() === value); - - // If no device is found, allow the value - if (!device) { - return true; - } - - // Check the type of the device - const type = device.getType(); - - // Consider any Wi-Fi / wireless / wlan device as invalid - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); - - // Allow only non-wireless devices - return !isWireless; - }; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'mon_restart_ifaces', - _('Interface monitoring'), - _('Interface monitoring for bad WAN'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - widgets.NetworkSelect, - 'restart_ifaces', - _('Interface for monitoring'), - _('Select the WAN interfaces to be monitored'), - ); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.multiple = true; - o.filter = function (section_id, value) { - // Reject if the value is in the blocked list ['lan', 'loopback'] - if (['lan', 'loopback'].includes(value)) { - return false; - } - - // Reject if the value starts with '@' (means it's an alias/reference) - if (value.startsWith('@')) { - return false; - } - - // Otherwise allow it - return true; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'procd_reload_delay', - _('Interface Monitoring Delay'), - _('Delay in milliseconds before reloading podkop after interface UP'), - ); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.default = '2000'; - o.rmempty = false; - o.validate = function (section_id, value) { - if (!value) { - return _('Delay value cannot be empty'); - } - return true; - }; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'dont_touch_dhcp', - _('Dont touch my DHCP!'), - _('Podkop will not change the DHCP config'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'detour', - _('Proxy download of lists'), - _('Downloading all lists via main Proxy/VPN'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - // Extra IPs and exclusions (main section) - o = mainSection.taboption( - 'basic', - form.Flag, - 'exclude_from_ip_enabled', - _('IP for exclusion'), - _('Specify local IP addresses that will never use the configured route'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'basic', - form.DynamicList, - 'exclude_traffic_ip', - _('Local IPs'), - _('Enter valid IPv4 addresses'), - ); - o.placeholder = 'IP'; - o.depends('exclude_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateIPV4(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'basic', - form.Flag, - 'socks5', - _('Mixed enable'), - _('Browser port: 2080'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; -} - -return baseclass.extend({ - createAdditionalSection, -}); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js deleted file mode 100644 index 64c1134..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ /dev/null @@ -1,779 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require network'; -'require view.podkop.main as main'; -'require tools.widgets as widgets'; - -function createConfigSection(section) { - const s = section; - - let o = s.tab('basic', _('Basic Settings')); - - o = s.taboption( - 'basic', - form.ListValue, - 'mode', - _('Connection Type'), - _('Select between VPN and Proxy connection methods for traffic routing'), - ); - o.value('proxy', 'Proxy'); - o.value('vpn', 'VPN'); - o.value('block', 'Block'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.ListValue, - 'proxy_config_type', - _('Configuration Type'), - _('Select how to configure the proxy'), - ); - o.value('url', _('Connection URL')); - o.value('outbound', _('Outbound Config')); - o.value('urltest', _('URLTest')); - o.default = 'url'; - o.depends('mode', 'proxy'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.TextValue, - 'proxy_string', - _('Proxy Configuration URL'), - '', - ); - o.depends('proxy_config_type', 'url'); - o.rows = 5; - // Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links) - o.wrap = 'soft'; - // Render as a textarea to allow multiple proxy URLs/configs - o.textarea = true; - o.rmempty = false; - o.ucisection = s.section; - o.sectionDescriptions = new Map(); - o.placeholder = - 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none'; - - o.renderWidget = function (section_id, option_index, cfgvalue) { - const original = form.TextValue.prototype.renderWidget.apply(this, [ - section_id, - option_index, - cfgvalue, - ]); - const container = E('div', {}); - container.appendChild(original); - - if (cfgvalue) { - try { - const activeConfig = cfgvalue - .split('\n') - .map((line) => line.trim()) - .find((line) => line && !line.startsWith('//')); - - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - const decodedLabel = decodeURIComponent(label); - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Current config: ') + decodedLabel, - ); - container.appendChild(descDiv); - } else { - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } else { - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } - } catch (e) { - console.error('Error parsing config label:', e); - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } else { - const defaultDesc = E( - 'div', - { class: 'cbi-value-description' }, - _( - 'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs', - ), - ); - container.appendChild(defaultDesc); - } - - return container; - }; - - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - try { - const activeConfigs = main.splitProxyString(value); - - if (!activeConfigs.length) { - return _( - 'No active configuration found. One configuration is required.', - ); - } - - if (activeConfigs.length > 1) { - return _( - 'Multiply active configurations found. Please leave one configuration.', - ); - } - - const validation = main.validateProxyUrl(activeConfigs[0]); - - if (validation.valid) { - return true; - } - - return validation.message; - } catch (e) { - return `${_('Invalid URL format:')} ${e?.message}`; - } - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'outbound_json', - _('Outbound Configuration'), - _('Enter complete outbound configuration in JSON format'), - ); - o.depends('proxy_config_type', 'outbound'); - o.rows = 10; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateOutboundJson(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.DynamicList, - 'urltest_proxy_links', - _('URLTest Proxy Links'), - ); - o.depends('proxy_config_type', 'urltest'); - o.placeholder = 'vless://, ss://, trojan:// links'; - o.rmempty = false; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateProxyUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'ss_uot', - _('Shadowsocks UDP over TCP'), - _('Apply for SS2022'), - ); - o.default = '0'; - o.depends('mode', 'proxy'); - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - widgets.DeviceSelect, - 'interface', - _('Network Interface'), - _('Select network interface for VPN connection'), - ); - o.depends('mode', 'vpn'); - o.ucisection = s.section; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.filter = function (section_id, value) { - // Blocked interface names that should never be selectable - const blockedInterfaces = [ - 'br-lan', - 'eth0', - 'eth1', - 'wan', - 'phy0-ap0', - 'phy1-ap0', - 'pppoe-wan', - 'lan', - ]; - - // Reject immediately if the value matches any blocked interface - if (blockedInterfaces.includes(value)) { - return false; - } - - // Try to find the device object with the given name - const device = this.devices.find((dev) => dev.getName() === value); - - // If no device is found, allow the value - if (!device) { - return true; - } - - // Get the device type (e.g., "wifi", "ethernet", etc.) - const type = device.getType(); - - // Reject wireless-related devices - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); - - return !isWireless; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'domain_resolver_enabled', - _('Domain Resolver'), - _('Enable built-in DNS resolver for domains handled by this section'), - ); - o.default = '0'; - o.rmempty = false; - o.depends('mode', 'vpn'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.ListValue, - 'domain_resolver_dns_type', - _('DNS Protocol Type'), - _('Select the DNS protocol type for the domain resolver'), - ); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.Value, - 'domain_resolver_dns_server', - _('DNS Server'), - _('Select or enter DNS server address'), - ); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'community_lists_enabled', - _('Community Lists'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'community_lists', - _('Service List'), - _('Select predefined service for routing') + - ' github.com/itdoginfo/allow-domains', - ); - o.placeholder = 'Service list'; - Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.depends('community_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - - let lastValues = []; - let isProcessing = false; - - o.onchange = function (ev, section_id, value) { - if (isProcessing) return; - isProcessing = true; - - try { - const values = Array.isArray(value) ? value : [value]; - let newValues = [...values]; - let notifications = []; - - const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => - newValues.includes(opt), - ); - - if (selectedRegionalOptions.length > 1) { - const lastSelected = - selectedRegionalOptions[selectedRegionalOptions.length - 1]; - const removedRegions = selectedRegionalOptions.slice(0, -1); - newValues = newValues.filter( - (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), - ); - notifications.push( - E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Regional options cannot be used together')), - E('br'), - _( - 'Warning: %s cannot be used together with %s. Previous selections have been removed.', - ).format(removedRegions.join(', '), lastSelected), - ]), - ); - } - - if (newValues.includes('russia_inside')) { - const removedServices = newValues.filter( - (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), - ); - if (removedServices.length > 0) { - newValues = newValues.filter((v) => - main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), - ); - notifications.push( - E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Russia inside restrictions')), - E('br'), - _( - 'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.', - ).format( - main.ALLOWED_WITH_RUSSIA_INSIDE.map( - (key) => main.DOMAIN_LIST_OPTIONS[key], - ) - .filter((label) => label !== 'Russia inside') - .join(', '), - removedServices.join(', '), - ), - ]), - ); - } - } - - if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { - this.getUIElement(section_id).setValue(newValues); - } - - notifications.forEach((notification) => - ui.addNotification(null, notification), - ); - lastValues = newValues; - } catch (e) { - console.error('Error in onchange handler:', e); - } finally { - isProcessing = false; - } - }; - - o = s.taboption( - 'basic', - form.ListValue, - 'user_domain_list_type', - _('User Domain List Type'), - _('Select how to add your custom domains'), - ); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'user_domains', - _('User Domains'), - _( - 'Enter domain names without protocols (example: sub.example.com or example.com)', - ), - ); - o.placeholder = 'Domains list'; - o.depends('user_domain_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateDomain(value, true); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'user_domains_text', - _('User Domains List'), - _( - 'Enter domain names separated by comma, space or newline. You can add comments after //', - ), - ); - o.placeholder = - 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; - o.depends('user_domain_list_type', 'text'); - o.rows = 8; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const domains = main.parseValueList(value); - - if (!domains.length) { - return _( - 'At least one valid domain must be specified. Comments-only content is not allowed.', - ); - } - - const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true)); - - if (!valid) { - const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors - - return [_('Validation errors:'), ...errors].join('\n'); - } - - return true; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'local_domain_lists_enabled', - _('Local Domain Lists'), - _('Use the list from the router filesystem'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'local_domain_lists', - _('Local Domain List Paths'), - _('Enter the list file path'), - ); - o.placeholder = '/path/file.lst'; - o.depends('local_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validatePath(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'remote_domain_lists_enabled', - _('Remote Domain Lists'), - _('Download and use domain lists from remote URLs'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'remote_domain_lists', - _('Remote Domain URLs'), - _('Enter full URLs starting with http:// or https://'), - ); - o.placeholder = 'URL'; - o.depends('remote_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'local_subnet_lists_enabled', - _('Local Subnet Lists'), - _('Use the list from the router filesystem'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'local_subnet_lists', - _('Local Subnet List Paths'), - _('Enter the list file path'), - ); - o.placeholder = '/path/file.lst'; - o.depends('local_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validatePath(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.ListValue, - 'user_subnet_list_type', - _('User Subnet List Type'), - _('Select how to add your custom subnets'), - ); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List (comma/space/newline separated)')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'user_subnets', - _('User Subnets'), - _( - 'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses', - ), - ); - o.placeholder = 'IP or subnet'; - o.depends('user_subnet_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateSubnet(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'user_subnets_text', - _('User Subnets List'), - _( - 'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //', - ), - ); - o.placeholder = - '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9'; - o.depends('user_subnet_list_type', 'text'); - o.rows = 10; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const subnets = main.parseValueList(value); - - if (!subnets.length) { - return _( - 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', - ); - } - - const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); - - if (!valid) { - const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors - - return [_('Validation errors:'), ...errors].join('\n'); - } - - return true; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'remote_subnet_lists_enabled', - _('Remote Subnet Lists'), - _('Download and use subnet lists from remote URLs'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'remote_subnet_lists', - _('Remote Subnet URLs'), - _('Enter full URLs starting with http:// or https://'), - ); - o.placeholder = 'URL'; - o.depends('remote_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'all_traffic_from_ip_enabled', - _('IP for full redirection'), - _( - 'Specify local IP addresses whose traffic will always use the configured route', - ), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'all_traffic_ip', - _('Local IPs'), - _('Enter valid IPv4 addresses'), - ); - o.placeholder = 'IP'; - o.depends('all_traffic_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateSubnet(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; -} - -return baseclass.extend({ - createConfigSection, -}); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js new file mode 100644 index 0000000..6fd97cf --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js @@ -0,0 +1,22 @@ +"use strict"; +"require baseclass"; +"require form"; +"require ui"; +"require uci"; +"require fs"; +"require view.podkop.main as main"; + +function createDashboardContent(section) { + const o = section.option(form.DummyValue, "_mount_node"); + o.rawhtml = true; + o.cfgvalue = () => { + main.DashboardTab.initController(); + return main.DashboardTab.render(); + }; +} + +const EntryPoint = { + createDashboardContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js deleted file mode 100644 index a5056dc..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require uci'; -'require fs'; -'require view.podkop.utils as utils'; -'require view.podkop.main as main'; - -function createDashboardSection(mainSection) { - let o = mainSection.tab('dashboard', _('Dashboard')); - - o = mainSection.taboption('dashboard', form.DummyValue, '_status'); - o.rawhtml = true; - o.cfgvalue = () => { - main.initDashboardController(); - - return main.renderDashboard(); - }; -} - -const EntryPoint = { - createDashboardSection, -}; - -return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js new file mode 100644 index 0000000..e6ac146 --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js @@ -0,0 +1,22 @@ +"use strict"; +"require baseclass"; +"require form"; +"require ui"; +"require uci"; +"require fs"; +"require view.podkop.main as main"; + +function createDiagnosticContent(section) { + const o = section.option(form.DummyValue, "_mount_node"); + o.rawhtml = true; + o.cfgvalue = () => { + main.DiagnosticTab.initController(); + return main.DiagnosticTab.render(); + }; +} + +const EntryPoint = { + createDiagnosticContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js deleted file mode 100644 index 1de1f76..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js +++ /dev/null @@ -1,1220 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require uci'; -'require fs'; -'require view.podkop.utils as utils'; -'require view.podkop.main as main'; - -// Cache system for network requests -const fetchCache = {}; - -// Helper function to fetch with cache -async function cachedFetch(url, options = {}) { - const cacheKey = url; - const currentTime = Date.now(); - - // If we have a valid cached response, return it - if ( - fetchCache[cacheKey] && - currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT - ) { - console.log(`Using cached response for ${url}`); - return Promise.resolve(fetchCache[cacheKey].response.clone()); - } - - // Otherwise, make a new request - try { - const response = await fetch(url, options); - - // Cache the response - fetchCache[cacheKey] = { - response: response.clone(), - timestamp: currentTime, - }; - - return response; - } catch (error) { - throw error; - } -} - -// Helper functions for command execution with prioritization - Using from utils.js now -function safeExec( - command, - args, - priority, - callback, - timeout = main.COMMAND_TIMEOUT, -) { - return utils.safeExec(command, args, priority, callback, timeout); -} - -// Helper functions for handling checks -function runCheck(checkFunction, priority, callback) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - const executeCheck = async () => { - try { - const result = await checkFunction(); - if (callback && typeof callback === 'function') { - callback(result); - } - return result; - } catch (error) { - if (callback && typeof callback === 'function') { - callback({ error }); - } - return { error }; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCheck, schedulingDelay); - return; - } else { - return executeCheck(); - } -} - -function runAsyncTask(taskFunction, priority) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - setTimeout(async () => { - try { - await taskFunction(); - } catch (error) { - console.error('Async task error:', error); - } - }, schedulingDelay); -} - -// Helper Functions for UI and formatting -function createStatus(state, message, color) { - return { - state, - message: _(message), - color: main.STATUS_COLORS[color], - }; -} - -function formatDiagnosticOutput(output) { - if (typeof output !== 'string') return ''; - return output - .trim() - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n'); -} - -function copyToClipboard(text, button) { - const textarea = document.createElement('textarea'); - textarea.value = text; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - const originalText = button.textContent; - button.textContent = _('Copied!'); - setTimeout( - () => (button.textContent = originalText), - main.BUTTON_FEEDBACK_TIMEOUT, - ); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); -} - -// IP masking function -function maskIP(ip) { - if (!ip) return ''; - const parts = ip.split('.'); - if (parts.length !== 4) return ip; - return ['XX', 'XX', 'XX', parts[3]].join('.'); -} - -// Status Check Functions -async function checkFakeIP() { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); - - try { - const response = await cachedFetch( - `https://${main.FAKEIP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data = await response.json(); - clearTimeout(timeoutId); - - if (data.fakeip === true) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus('not_working', 'not working', 'ERROR'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = - fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); - } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); - } -} - -async function checkFakeIPCLI() { - try { - return new Promise((resolve) => { - safeExec( - 'nslookup', - ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], - 'P0_PRIORITY', - (result) => { - if (result.stdout && result.stdout.includes('198.18')) { - resolve(createStatus('working', 'working on router', 'SUCCESS')); - } else { - resolve( - createStatus('not_working', 'not working on router', 'ERROR'), - ); - } - }, - ); - }); - } catch (error) { - return createStatus('error', 'CLI check error', 'WARNING'); - } -} - -function checkDNSAvailability() { - return new Promise(async (resolve) => { - try { - safeExec( - '/usr/bin/podkop', - ['check_dns_available'], - 'P0_PRIORITY', - (dnsStatusResult) => { - if (!dnsStatusResult || !dnsStatusResult.stdout) { - return resolve({ - remote: createStatus('error', 'DNS check timeout', 'WARNING'), - local: createStatus('error', 'DNS check timeout', 'WARNING'), - }); - } - - try { - const dnsStatus = JSON.parse(dnsStatusResult.stdout); - - const remoteStatus = dnsStatus.is_available - ? createStatus( - 'available', - `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, - 'SUCCESS', - ) - : createStatus( - 'unavailable', - `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, - 'ERROR', - ); - - const localStatus = dnsStatus.local_dns_working - ? createStatus('available', 'Router DNS working', 'SUCCESS') - : createStatus('unavailable', 'Router DNS not working', 'ERROR'); - - return resolve({ - remote: remoteStatus, - local: localStatus, - }); - } catch (parseError) { - return resolve({ - remote: createStatus('error', 'DNS check parse error', 'WARNING'), - local: createStatus('error', 'DNS check parse error', 'WARNING'), - }); - } - }, - ); - } catch (error) { - return resolve({ - remote: createStatus('error', 'DNS check error', 'WARNING'), - local: createStatus('error', 'DNS check error', 'WARNING'), - }); - } - }); -} - -async function checkBypass() { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); - - try { - const response1 = await cachedFetch( - `https://${main.FAKEIP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data1 = await response1.json(); - - const response2 = await cachedFetch( - `https://${main.IP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data2 = await response2.json(); - - clearTimeout(timeoutId); - - if (data1.IP && data2.IP) { - if (data1.IP !== data2.IP) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus( - 'not_working', - 'same IP for both domains', - 'ERROR', - ); - } - } else { - return createStatus('error', 'check error (no IP)', 'WARNING'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = - fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); - } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); - } -} - -function showConfigModal(command, title) { - // Create and show modal immediately with loading state - const modalContent = E('div', { class: 'panel-body' }, [ - E( - 'div', - { - class: 'panel-body', - style: - 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + - 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + - 'line-height: 1.5; font-size: 14px;', - }, - [ - E( - 'pre', - { - id: 'modal-content-pre', - style: 'margin: 0;', - }, - _('Loading...'), - ), - ], - ), - E( - 'div', - { - class: 'right', - style: 'margin-top: 1em;', - }, - [ - E( - 'button', - { - class: 'btn', - id: 'copy-button', - click: (ev) => - copyToClipboard( - '```txt\n' + - document.getElementById('modal-content-pre').innerText + - '\n```', - ev.target, - ), - }, - _('Copy to Clipboard'), - ), - E( - 'button', - { - class: 'btn', - click: ui.hideModal, - }, - _('Close'), - ), - ], - ), - ]); - - ui.showModal(_(title), modalContent); - - // Function to update modal content - const updateModalContent = (content) => { - const pre = document.getElementById('modal-content-pre'); - if (pre) { - pre.textContent = content; - } - }; - - try { - let formattedOutput = ''; - - if (command === 'global_check') { - safeExec('/usr/bin/podkop', [command, `${main.PODKOP_LUCI_APP_VERSION}`], 'P0_PRIORITY', (res) => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - - try { - const controller = new AbortController(); - const timeoutId = setTimeout( - () => controller.abort(), - main.FETCH_TIMEOUT, - ); - - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((response) => response.json()) - .then((data) => { - clearTimeout(timeoutId); - - if (data.fakeip === true) { - formattedOutput += - '\n✅ ' + _('FakeIP is working in browser!') + '\n'; - } else { - formattedOutput += - '\n❌ ' + _('FakeIP is not working in browser') + '\n'; - formattedOutput += - _('Check DNS server on current device (PC, phone)') + '\n'; - formattedOutput += _('Its must be router!') + '\n'; - } - - // Bypass check - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((bypassResponse) => bypassResponse.json()) - .then((bypassData) => { - cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((bypassResponse2) => bypassResponse2.json()) - .then((bypassData2) => { - formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; - - if ( - bypassData.IP && - bypassData2.IP && - bypassData.IP !== bypassData2.IP - ) { - formattedOutput += - '✅ ' + _('Proxy working correctly') + '\n'; - formattedOutput += - _('Direct IP: ') + maskIP(bypassData.IP) + '\n'; - formattedOutput += - _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n'; - } else if (bypassData.IP === bypassData2.IP) { - formattedOutput += - '❌ ' + - _('Proxy is not working - same IP for both domains') + - '\n'; - formattedOutput += - _('IP: ') + maskIP(bypassData.IP) + '\n'; - } else { - formattedOutput += - '❌ ' + _('Proxy check failed') + '\n'; - } - - updateModalContent(formattedOutput); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' - ? _('timeout') - : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' - ? _('timeout') - : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' ? _('timeout') : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - } catch (error) { - formattedOutput += - '\n❌ ' + _('Check failed: ') + error.message + '\n'; - updateModalContent(formattedOutput); - } - }); - } else { - safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - updateModalContent(formattedOutput); - }); - } - } catch (error) { - updateModalContent(_('Error: ') + error.message); - } -} - -// Button Factory -const ButtonFactory = { - createButton: function (config) { - return E( - 'button', - { - class: `btn ${config.additionalClass || ''}`.trim(), - click: config.onClick, - style: config.style || '', - }, - _(config.label), - ); - }, - - createActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => - safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY').then( - () => config.reload && location.reload(), - ), - style: config.style, - }); - }, - - createInitActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => - safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY').then( - () => config.reload && location.reload(), - ), - style: config.style, - }); - }, - - createModalButton: function (config) { - return this.createButton({ - label: config.label, - onClick: () => showConfigModal(config.command, config.title), - additionalClass: `cbi-button-${config.type || ''}`, - style: config.style, - }); - }, -}; - -// Create a loading placeholder for status text -function createLoadingStatusText() { - return E('span', { class: 'loading-indicator' }, _('Loading...')); -} - -// Create the status section with buttons loaded immediately but status indicators loading asynchronously -let createStatusSection = async function () { - // Get initial podkop status - let initialPodkopStatus = { enabled: false }; - try { - const result = await fs.exec('/usr/bin/podkop', ['get_status']); - if (result && result.stdout) { - const status = JSON.parse(result.stdout); - initialPodkopStatus.enabled = status.enabled === 1; - } - } catch (e) { - console.error('Error getting initial podkop status:', e); - } - - return E('div', { class: 'cbi-section' }, [ - E('div', { class: 'table', style: 'display: flex; gap: 20px;' }, [ - // Podkop Status Panel - E( - 'div', - { - id: 'podkop-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Podkop Status')), - E('br'), - E('span', { id: 'podkop-status-text' }, createLoadingStatusText()), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - ButtonFactory.createActionButton({ - label: 'Restart Podkop', - type: 'apply', - action: 'restart', - reload: true, - }), - ButtonFactory.createActionButton({ - label: 'Stop Podkop', - type: 'apply', - action: 'stop', - reload: true, - }), - // Autostart button - create with initial state - ButtonFactory.createInitActionButton({ - label: initialPodkopStatus.enabled - ? 'Disable Autostart' - : 'Enable Autostart', - type: initialPodkopStatus.enabled ? 'remove' : 'apply', - action: initialPodkopStatus.enabled ? 'disable' : 'enable', - reload: true, - }), - ButtonFactory.createModalButton({ - label: _('Global check'), - command: 'global_check', - title: _('Click here for all the info'), - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_logs', - title: 'Podkop Logs', - }), - ButtonFactory.createModalButton({ - label: _('Update Lists'), - command: 'list_update', - title: _('Lists Update Results'), - }), - ], - ), - ], - ), - - // Sing-box Status Panel - E( - 'div', - { - id: 'singbox-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Sing-box Status')), - E('br'), - E('span', { id: 'singbox-status-text' }, createLoadingStatusText()), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - ButtonFactory.createModalButton({ - label: 'Show Config', - command: 'show_sing_box_config', - title: 'Sing-box Configuration', - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_sing_box_logs', - title: 'Sing-box Logs', - }), - ButtonFactory.createModalButton({ - label: 'Check Connections', - command: 'check_sing_box_connections', - title: 'Active Connections', - }), - ButtonFactory.createModalButton({ - label: _('Check NFT Rules'), - command: 'check_nft', - title: _('NFT Rules'), - }), - ButtonFactory.createModalButton({ - label: _('Check DNSMasq'), - command: 'check_dnsmasq', - title: _('DNSMasq Configuration'), - }), - ], - ), - ], - ), - - // FakeIP Status Panel - E( - 'div', - { - id: 'fakeip-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('FakeIP Status')), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E( - 'span', - { id: 'fakeip-browser-status' }, - createLoadingStatusText(), - ), - ]), - E('div', {}, [ - E( - 'span', - { id: 'fakeip-router-status' }, - createLoadingStatusText(), - ), - ]), - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', {}, _('DNS Status')), - E('br'), - E( - 'span', - { id: 'dns-remote-status' }, - createLoadingStatusText(), - ), - E('br'), - E( - 'span', - { id: 'dns-local-status' }, - createLoadingStatusText(), - ), - ]), - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', { id: 'config-name-text' }, _('Main config')), - E('br'), - E('span', { id: 'bypass-status' }, createLoadingStatusText()), - ]), - ]), - ], - ), - ], - ), - - // Version Information Panel - E( - 'div', - { - id: 'version-info-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Version Information')), - ]), - E('div', { class: 'panel-body' }, [ - E( - 'div', - { - style: - 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;', - }, - [ - E('strong', {}, _('Podkop: ')), - E('span', { id: 'podkop-version' }, _('Loading...')), - '\n', - E('strong', {}, _('LuCI App: ')), - E('span', { id: 'luci-version' }, _('Loading...')), - '\n', - E('strong', {}, _('Sing-box: ')), - E('span', { id: 'singbox-version' }, _('Loading...')), - '\n', - E('strong', {}, _('OpenWrt Version: ')), - E('span', { id: 'openwrt-version' }, _('Loading...')), - '\n', - E('strong', {}, _('Device Model: ')), - E('span', { id: 'device-model' }, _('Loading...')), - ], - ), - ]), - ], - ), - ]), - ]); -}; - -// Global variables for tracking state -let diagnosticsUpdateTimer = null; -let isInitialCheck = true; -showConfigModal.busy = false; - -function startDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - } - - // Immediately update when started - updateDiagnostics(); - - // Then set up periodic updates - diagnosticsUpdateTimer = setInterval( - updateDiagnostics, - main.DIAGNOSTICS_UPDATE_INTERVAL, - ); -} - -function stopDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - diagnosticsUpdateTimer = null; - } -} - -// Update individual text element with new content -function updateTextElement(elementId, content) { - const element = document.getElementById(elementId); - if (element) { - element.innerHTML = ''; - element.appendChild(content); - } -} - -async function updateDiagnostics() { - // Podkop Status check - safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', (result) => { - try { - const parsedPodkopStatus = JSON.parse( - result.stdout || '{"enabled":0,"status":"error"}', - ); - - // Update Podkop status text - updateTextElement( - 'podkop-status-text', - E( - 'span', - { - style: `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`, - }, - [ - parsedPodkopStatus.enabled - ? '✔ Autostart enabled' - : '✘ Autostart disabled', - ], - ), - ); - - // Update autostart button - const autostartButton = parsedPodkopStatus.enabled - ? ButtonFactory.createInitActionButton({ - label: 'Disable Autostart', - type: 'remove', - action: 'disable', - reload: true, - }) - : ButtonFactory.createInitActionButton({ - label: 'Enable Autostart', - type: 'apply', - action: 'enable', - reload: true, - }); - - // Find the autostart button and replace it - const panel = document.getElementById('podkop-status-panel'); - if (panel) { - const buttons = panel.querySelectorAll('.cbi-button'); - if (buttons.length >= 3) { - buttons[2].parentNode.replaceChild(autostartButton, buttons[2]); - } - } - } catch (error) { - updateTextElement( - 'podkop-status-text', - E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), - ); - } - }); - - // Sing-box Status check - safeExec( - '/usr/bin/podkop', - ['get_sing_box_status'], - 'P0_PRIORITY', - (result) => { - try { - const parsedSingboxStatus = JSON.parse( - result.stdout || '{"running":0,"enabled":0,"status":"error"}', - ); - - // Update Sing-box status text - updateTextElement( - 'singbox-status-text', - E( - 'span', - { - style: `color: ${ - parsedSingboxStatus.running && !parsedSingboxStatus.enabled - ? main.STATUS_COLORS.SUCCESS - : main.STATUS_COLORS.ERROR - }`, - }, - [ - parsedSingboxStatus.running && !parsedSingboxStatus.enabled - ? '✔ running' - : '✘ ' + parsedSingboxStatus.status, - ], - ), - ); - } catch (error) { - updateTextElement( - 'singbox-status-text', - E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), - ); - } - }, - ); - - // Version Information checks - safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', (result) => { - updateTextElement( - 'podkop-version', - document.createTextNode( - result.stdout ? result.stdout.trim() : _('Unknown'), - ), - ); - }); - - updateTextElement( - 'luci-version', - document.createTextNode( - `${main.PODKOP_LUCI_APP_VERSION}` - ) - ); - - safeExec( - '/usr/bin/podkop', - ['show_sing_box_version'], - 'P2_PRIORITY', - (result) => { - updateTextElement( - 'singbox-version', - document.createTextNode( - result.stdout ? result.stdout.trim() : _('Unknown'), - ), - ); - }, - ); - - safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', (result) => { - if (result.stdout) { - updateTextElement( - 'openwrt-version', - document.createTextNode(result.stdout.split('\n')[1].trim()), - ); - updateTextElement( - 'device-model', - document.createTextNode(result.stdout.split('\n')[4].trim()), - ); - } else { - updateTextElement( - 'openwrt-version', - document.createTextNode(_('Unknown')), - ); - updateTextElement('device-model', document.createTextNode(_('Unknown'))); - } - }); - - // FakeIP and DNS status checks - runCheck(checkFakeIP, 'P3_PRIORITY', (result) => { - updateTextElement( - 'fakeip-browser-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error - ? 'check error' - : result.state === 'working' - ? _('works in browser') - : _('does not work in browser'), - ], - ), - ); - }); - - runCheck(checkFakeIPCLI, 'P8_PRIORITY', (result) => { - updateTextElement( - 'fakeip-router-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error - ? 'check error' - : result.state === 'working' - ? _('works on router') - : _('does not work on router'), - ], - ), - ); - }); - - runCheck(checkDNSAvailability, 'P4_PRIORITY', (result) => { - if (result.error) { - updateTextElement( - 'dns-remote-status', - E( - 'span', - { style: `color: ${main.STATUS_COLORS.WARNING}` }, - '! DNS check error', - ), - ); - updateTextElement( - 'dns-local-status', - E( - 'span', - { style: `color: ${main.STATUS_COLORS.WARNING}` }, - '! DNS check error', - ), - ); - } else { - updateTextElement( - 'dns-remote-status', - E('span', { style: `color: ${result.remote.color}` }, [ - result.remote.state === 'available' - ? '✔ ' - : result.remote.state === 'unavailable' - ? '✘ ' - : '! ', - result.remote.message, - ]), - ); - - updateTextElement( - 'dns-local-status', - E('span', { style: `color: ${result.local.color}` }, [ - result.local.state === 'available' - ? '✔ ' - : result.local.state === 'unavailable' - ? '✘ ' - : '! ', - result.local.message, - ]), - ); - } - }); - - runCheck( - checkBypass, - 'P1_PRIORITY', - (result) => { - updateTextElement( - 'bypass-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error ? 'check error' : result.message, - ], - ), - ); - }, - 'P1_PRIORITY', - ); - - // Config name - runAsyncTask(async () => { - try { - let configName = _('Main config'); - const data = await uci.load('podkop'); - const proxyString = uci.get('podkop', 'main', 'proxy_string'); - - if (proxyString) { - const activeConfig = proxyString - .split('\n') - .map((line) => line.trim()) - .find((line) => line && !line.startsWith('//')); - - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - configName = _('Config: ') + decodeURIComponent(label); - } - } - } - } - - updateTextElement( - 'config-name-text', - document.createTextNode(configName), - ); - } catch (e) { - console.error('Error getting config name from UCI:', e); - } - }, 'P1_PRIORITY'); -} - -function createDiagnosticsSection(mainSection) { - let o = mainSection.tab('diagnostics', _('Diagnostics')); - - o = mainSection.taboption('diagnostics', form.DummyValue, '_status'); - o.rawhtml = true; - o.cfgvalue = () => - E('div', { - id: 'diagnostics-status', - 'data-loading': 'true', - }); -} - -function setupDiagnosticsEventHandlers(node) { - const titleDiv = E('h2', { class: 'cbi-map-title' }, _('Podkop')); - node.insertBefore(titleDiv, node.firstChild); - - // Function to initialize diagnostics - function initDiagnostics(container) { - if (container && container.hasAttribute('data-loading')) { - container.innerHTML = ''; - showConfigModal.busy = false; - createStatusSection().then((section) => { - container.appendChild(section); - startDiagnosticsUpdates(); - // Start error polling when diagnostics tab is active - utils.startErrorPolling(); - }); - } - } - - document.addEventListener('visibilitychange', function () { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector( - '.cbi-tab[data-tab="diagnostics"]', - ); - - if ( - document.hidden || - !diagnosticsTab || - !diagnosticsTab.classList.contains('cbi-tab-active') - ) { - stopDiagnosticsUpdates(); - // Don't stop error polling here - it's managed in podkop.js for all tabs - } else if ( - diagnosticsContainer && - diagnosticsContainer.hasAttribute('data-loading') - ) { - startDiagnosticsUpdates(); - // Ensure error polling is running when diagnostics tab is active - utils.startErrorPolling(); - } - }); - - setTimeout(() => { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector( - '.cbi-tab[data-tab="diagnostics"]', - ); - const otherTabs = document.querySelectorAll( - '.cbi-tab:not([data-tab="diagnostics"])', - ); - - // Check for direct page load case - const noActiveTabsExist = !Array.from(otherTabs).some((tab) => - tab.classList.contains('cbi-tab-active'), - ); - - if ( - diagnosticsContainer && - diagnosticsTab && - (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist) - ) { - initDiagnostics(diagnosticsContainer); - } - - const tabs = node.querySelectorAll('.cbi-tabmenu'); - if (tabs.length > 0) { - tabs[0].addEventListener('click', function (e) { - const tab = e.target.closest('.cbi-tab'); - if (tab) { - const tabName = tab.getAttribute('data-tab'); - if (tabName === 'diagnostics') { - const container = document.getElementById('diagnostics-status'); - container.setAttribute('data-loading', 'true'); - initDiagnostics(container); - } else { - stopDiagnosticsUpdates(); - // Don't stop error polling - it should continue on all tabs - } - } - }); - } - }, main.DIAGNOSTICS_INITIAL_DELAY); - - node.classList.add('fade-in'); - return node; -} - -return baseclass.extend({ - createDiagnosticsSection, - setupDiagnosticsEventHandlers, -}); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 2f5f206..3042a78 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -3,6 +3,7 @@ "require baseclass"; "require fs"; "require uci"; +"require ui"; // src/validators/validateIp.ts function validateIPV4(ip) { @@ -210,10 +211,264 @@ function validateShadowsocksUrl(url) { return { valid: true, message: _("Valid") }; } -// src/helpers/getBaseUrl.ts -function getBaseUrl() { - const { protocol, hostname } = window.location; - return `${protocol}//${hostname}`; +// src/helpers/parseQueryString.ts +function parseQueryString(query) { + const clean = query.startsWith("?") ? query.slice(1) : query; + return clean.split("&").filter(Boolean).reduce( + (acc, pair) => { + const [rawKey, rawValue = ""] = pair.split("="); + if (!rawKey) { + return acc; + } + const key = decodeURIComponent(rawKey); + const value = decodeURIComponent(rawValue); + return { ...acc, [key]: value }; + }, + {} + ); +} + +// src/validators/validateVlessUrl.ts +function validateVlessUrl(url) { + try { + if (!url.startsWith("vless://")) + return { + valid: false, + message: "Invalid VLESS URL: must start with vless://" + }; + if (/\s/.test(url)) + return { + valid: false, + message: "Invalid VLESS URL: must not contain spaces" + }; + const body = url.slice("vless://".length); + const [mainPart] = body.split("#"); + const [userHostPort, queryString] = mainPart.split("?"); + if (!userHostPort) + return { + valid: false, + message: "Invalid VLESS URL: missing host and UUID" + }; + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userPart) + return { valid: false, message: "Invalid VLESS URL: missing UUID" }; + if (!hostPortPart) + return { valid: false, message: "Invalid VLESS URL: missing server" }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid VLESS URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid VLESS URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: "Invalid VLESS URL: invalid port number" + }; + if (!queryString) + return { + valid: false, + message: "Invalid VLESS URL: missing query parameters" + }; + const params = parseQueryString(queryString); + const validTypes = [ + "tcp", + "raw", + "udp", + "grpc", + "http", + "httpupgrade", + "xhttp", + "ws", + "kcp" + ]; + const validSecurities = ["tls", "reality", "none"]; + if (!params.type || !validTypes.includes(params.type)) + return { + valid: false, + message: "Invalid VLESS URL: unsupported or missing type" + }; + if (!params.security || !validSecurities.includes(params.security)) + return { + valid: false, + message: "Invalid VLESS URL: unsupported or missing security" + }; + if (params.security === "reality") { + if (!params.pbk) + return { + valid: false, + message: "Invalid VLESS URL: missing pbk for reality" + }; + if (!params.fp) + return { + valid: false, + message: "Invalid VLESS URL: missing fp for reality" + }; + } + if (params.flow === "xtls-rprx-vision-udp443") { + return { + valid: false, + message: "Invalid VLESS URL: flow xtls-rprx-vision-udp443 is not supported" + }; + } + return { valid: true, message: _("Valid") }; + } catch (_e) { + return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; + } +} + +// src/validators/validateOutboundJson.ts +function validateOutboundJson(value) { + try { + const parsed = JSON.parse(value); + if (!parsed.type || !parsed.server || !parsed.server_port) { + return { + valid: false, + message: _( + 'Outbound JSON must contain at least "type", "server" and "server_port" fields' + ) + }; + } + return { valid: true, message: _("Valid") }; + } catch { + return { valid: false, message: _("Invalid JSON format") }; + } +} + +// src/validators/validateTrojanUrl.ts +function validateTrojanUrl(url) { + try { + if (!url.startsWith("trojan://")) { + return { + valid: false, + message: _("Invalid Trojan URL: must start with trojan://") + }; + } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _("Invalid Trojan URL: must not contain spaces") + }; + } + const body = url.slice("trojan://".length); + const [mainPart] = body.split("#"); + const [userHostPort] = mainPart.split("?"); + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userHostPort) + return { + valid: false, + message: "Invalid Trojan URL: missing credentials and host" + }; + if (!userPart) + return { valid: false, message: "Invalid Trojan URL: missing password" }; + if (!hostPortPart) + return { + valid: false, + message: "Invalid Trojan URL: missing hostname and port" + }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid Trojan URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid Trojan URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: "Invalid Trojan URL: invalid port number" + }; + } catch (_e) { + return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; + } + return { valid: true, message: _("Valid") }; +} + +// src/validators/validateSocksUrl.ts +function validateSocksUrl(url) { + try { + if (!/^socks(4|4a|5):\/\//.test(url)) { + return { + valid: false, + message: _( + "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" + ) + }; + } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _("Invalid SOCKS URL: must not contain spaces") + }; + } + const body = url.replace(/^socks(4|4a|5):\/\//, ""); + const [authAndHost] = body.split("#"); + const [credentials, hostPortPart] = authAndHost.includes("@") ? authAndHost.split("@") : [null, authAndHost]; + if (credentials) { + const [username, _password] = credentials.split(":"); + if (!username) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing username") + }; + } + } + if (!hostPortPart) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing host and port") + }; + } + const [host, port] = hostPortPart.split(":"); + if (!host) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing hostname or IP") + }; + } + if (!port) { + return { valid: false, message: _("Invalid SOCKS URL: missing port") }; + } + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _("Invalid SOCKS URL: invalid port number") + }; + } + const ipv4Result = validateIPV4(host); + const domainResult = validateDomain(host); + if (!ipv4Result.valid && !domainResult.valid) { + return { + valid: false, + message: _("Invalid SOCKS URL: invalid host format") + }; + } + } catch (_e) { + return { valid: false, message: _("Invalid SOCKS URL: parsing failed") }; + } + return { valid: true, message: _("Valid") }; +} + +// src/validators/validateProxyUrl.ts +function validateProxyUrl(url) { + if (url.startsWith("ss://")) { + return validateShadowsocksUrl(url); + } + if (url.startsWith("vless://")) { + return validateVlessUrl(url); + } + if (url.startsWith("trojan://")) { + return validateTrojanUrl(url); + } + if (/^socks(4|4a|5):\/\//.test(url)) { + return validateSocksUrl(url); + } + return { + valid: false, + message: _( + "URL must start with vless://, ss://, trojan://, or socks4/5://" + ) + }; } // src/helpers/parseValueList.ts @@ -221,212 +476,267 @@ function parseValueList(value) { return value.split(/\n/).map((line) => line.split("//")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean); } -// src/styles.ts -var GlobalStyles = ` -.cbi-value { - margin-bottom: 10px !important; +// src/podkop/methods/custom/getConfigSections.ts +async function getConfigSections() { + return uci.load("podkop").then(() => uci.sections("podkop")); } -#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); -} - -#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra { - display: none; -} - -#cbi-podkop-main-_status > div { - width: 100%; -} - -/* Dashboard styles */ - -.pdk_dashboard-page { - width: 100%; - --dashboard-grid-columns: 4; -} - -@media (max-width: 900px) { - .pdk_dashboard-page { - --dashboard-grid-columns: 2; - } -} - -.pdk_dashboard-page__widgets-section { - margin-top: 10px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; -} - -.pdk_dashboard-page__widgets-section__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; -} - -.pdk_dashboard-page__widgets-section__item__title {} - -.pdk_dashboard-page__widgets-section__item__row {} - -.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--error-color-medium, red); -} - -.pdk_dashboard-page__widgets-section__item__row__key {} - -.pdk_dashboard-page__widgets-section__item__row__value {} - -.pdk_dashboard-page__outbound-section { - margin-top: 10px; - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; -} - -.pdk_dashboard-page__outbound-section__title-section { - display: flex; - align-items: center; - justify-content: space-between; -} - -.pdk_dashboard-page__outbound-section__title-section__title { - color: var(--text-color-high); - font-weight: 700; -} - -.pdk_dashboard-page__outbound-grid { - margin-top: 5px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; -} - -.pdk_dashboard-page__outbound-grid__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - transition: border 0.2s ease; -} - -.pdk_dashboard-page__outbound-grid__item--selectable { - cursor: pointer; -} - -.pdk_dashboard-page__outbound-grid__item--selectable:hover { - border-color: var(--primary-color-high, dodgerblue); -} - -.pdk_dashboard-page__outbound-grid__item--active { - border-color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__outbound-grid__item__footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 10px; -} - -.pdk_dashboard-page__outbound-grid__item__type {} - -.pdk_dashboard-page__outbound-grid__item__latency--empty { - color: var(--primary-color-low, lightgray); -} - -.pdk_dashboard-page__outbound-grid__item__latency--green { - color: var(--success-color-medium, green); -} - -.pdk_dashboard-page__outbound-grid__item__latency--yellow { - color: var(--warn-color-medium, orange); -} - -.pdk_dashboard-page__outbound-grid__item__latency--red { - color: var(--error-color-medium, red); -} - -.centered { - display: flex; - align-items: center; - justify-content: center; -} - -/* Skeleton styles*/ -.skeleton { - background-color: var(--background-color-low, #e0e0e0); - border-radius: 4px; - position: relative; - overflow: hidden; -} - -.skeleton::after { - content: ''; - position: absolute; - top: 0; - left: -150%; - width: 150%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.4), - transparent - ); - animation: skeleton-shimmer 1.6s infinite; -} - -@keyframes skeleton-shimmer { - 100% { - left: 150%; - } -} -`; - -// src/helpers/injectGlobalStyles.ts -function injectGlobalStyles() { - document.head.insertAdjacentHTML( - "beforeend", - ` - - ` - ); -} - -// src/helpers/withTimeout.ts -async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = _("Operation timed out")) { - let timeoutId; - const start = performance.now(); - const timeoutPromise = new Promise((_2, reject) => { - timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); +// src/podkop/methods/shell/callBaseMethod.ts +async function callBaseMethod(method, args = [], command = "/usr/bin/podkop") { + const response = await executeShellCommand({ + command, + args: [method, ...args], + timeout: 1e4 }); - try { - return await Promise.race([promise, timeoutPromise]); - } finally { - clearTimeout(timeoutId); - const elapsed = performance.now() - start; - console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`); + if (response.stdout) { + try { + return { + success: true, + data: JSON.parse(response.stdout) + }; + } catch (_e) { + return { + success: true, + data: response.stdout + }; + } } + return { + success: false, + error: "" + }; } +// src/podkop/types.ts +var Podkop; +((Podkop2) => { + let AvailableMethods; + ((AvailableMethods2) => { + AvailableMethods2["CHECK_DNS_AVAILABLE"] = "check_dns_available"; + AvailableMethods2["CHECK_FAKEIP"] = "check_fakeip"; + AvailableMethods2["CHECK_NFT_RULES"] = "check_nft_rules"; + AvailableMethods2["GET_STATUS"] = "get_status"; + AvailableMethods2["CHECK_SING_BOX"] = "check_sing_box"; + AvailableMethods2["GET_SING_BOX_STATUS"] = "get_sing_box_status"; + AvailableMethods2["CLASH_API"] = "clash_api"; + AvailableMethods2["RESTART"] = "restart"; + AvailableMethods2["START"] = "start"; + AvailableMethods2["STOP"] = "stop"; + AvailableMethods2["ENABLE"] = "enable"; + AvailableMethods2["DISABLE"] = "disable"; + AvailableMethods2["GLOBAL_CHECK"] = "global_check"; + AvailableMethods2["SHOW_SING_BOX_CONFIG"] = "show_sing_box_config"; + AvailableMethods2["CHECK_LOGS"] = "check_logs"; + AvailableMethods2["GET_SYSTEM_INFO"] = "get_system_info"; + })(AvailableMethods = Podkop2.AvailableMethods || (Podkop2.AvailableMethods = {})); + let AvailableClashAPIMethods; + ((AvailableClashAPIMethods2) => { + AvailableClashAPIMethods2["GET_PROXIES"] = "get_proxies"; + AvailableClashAPIMethods2["GET_PROXY_LATENCY"] = "get_proxy_latency"; + AvailableClashAPIMethods2["GET_GROUP_LATENCY"] = "get_group_latency"; + AvailableClashAPIMethods2["SET_GROUP_PROXY"] = "set_group_proxy"; + })(AvailableClashAPIMethods = Podkop2.AvailableClashAPIMethods || (Podkop2.AvailableClashAPIMethods = {})); +})(Podkop || (Podkop = {})); + +// src/podkop/methods/shell/index.ts +var PodkopShellMethods = { + checkDNSAvailable: async () => callBaseMethod( + Podkop.AvailableMethods.CHECK_DNS_AVAILABLE + ), + checkFakeIP: async () => callBaseMethod( + Podkop.AvailableMethods.CHECK_FAKEIP + ), + checkNftRules: async () => callBaseMethod( + Podkop.AvailableMethods.CHECK_NFT_RULES + ), + getStatus: async () => callBaseMethod(Podkop.AvailableMethods.GET_STATUS), + checkSingBox: async () => callBaseMethod( + Podkop.AvailableMethods.CHECK_SING_BOX + ), + getSingBoxStatus: async () => callBaseMethod( + Podkop.AvailableMethods.GET_SING_BOX_STATUS + ), + getClashApiProxies: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_PROXIES + ]), + getClashApiProxyLatency: async (tag) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, + tag + ]), + getClashApiGroupLatency: async (tag) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, + tag + ]), + setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ + Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY, + group, + proxy + ]), + restart: async () => callBaseMethod( + Podkop.AvailableMethods.RESTART, + [], + "/etc/init.d/podkop" + ), + start: async () => callBaseMethod( + Podkop.AvailableMethods.START, + [], + "/etc/init.d/podkop" + ), + stop: async () => callBaseMethod( + Podkop.AvailableMethods.STOP, + [], + "/etc/init.d/podkop" + ), + enable: async () => callBaseMethod( + Podkop.AvailableMethods.ENABLE, + [], + "/etc/init.d/podkop" + ), + disable: async () => callBaseMethod( + Podkop.AvailableMethods.DISABLE, + [], + "/etc/init.d/podkop" + ), + globalCheck: async () => callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), + showSingBoxConfig: async () => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), + checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), + getSystemInfo: async () => callBaseMethod( + Podkop.AvailableMethods.GET_SYSTEM_INFO + ) +}; + +// src/podkop/methods/custom/getDashboardSections.ts +async function getDashboardSections() { + const configSections = await getConfigSections(); + const clashProxies = await PodkopShellMethods.getClashApiProxies(); + if (!clashProxies.success) { + return { + success: false, + data: [] + }; + } + const proxies = Object.entries(clashProxies.data.proxies).map( + ([key, value]) => ({ + code: key, + value + }) + ); + const data = configSections.filter( + (section) => section.connection_type !== "block" && section[".type"] !== "settings" + ).map((section) => { + if (section.connection_type === "proxy") { + if (section.proxy_config_type === "url") { + const outbound = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-out` + ); + const activeConfigs = splitProxyString(section.proxy_string); + const proxyDisplayName = getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ""; + return { + withTagSelect: false, + code: outbound?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || section[".name"], + displayName: proxyDisplayName, + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || "", + selected: true + } + ] + }; + } + if (section.proxy_config_type === "outbound") { + const outbound = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-out` + ); + const parsedOutbound = JSON.parse(section.outbound_json); + const parsedTag = parsedOutbound?.tag ? decodeURIComponent(parsedOutbound?.tag) : void 0; + const proxyDisplayName = parsedTag || outbound?.value?.name || ""; + return { + withTagSelect: false, + code: outbound?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || section[".name"], + displayName: proxyDisplayName, + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || "", + selected: true + } + ] + }; + } + if (section.proxy_config_type === "urltest") { + const selector = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-out` + ); + const outbound = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-urltest-out` + ); + const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item, index) => ({ + code: item?.code || "", + displayName: getProxyUrlName(section.urltest_proxy_links?.[index]) || item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item?.code + })); + return { + withTagSelect: true, + code: selector?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || "", + displayName: _("Fastest"), + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || "", + selected: selector?.value?.now === outbound?.code + }, + ...outbounds + ] + }; + } + } + if (section.connection_type === "vpn") { + const outbound = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-out` + ); + return { + withTagSelect: false, + code: outbound?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || section[".name"], + displayName: section.interface || outbound?.value?.name || "", + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || "", + selected: true + } + ] + }; + } + return { + withTagSelect: false, + code: section[".name"], + displayName: section[".name"], + outbounds: [] + }; + }); + return { + success: true, + data + }; +} + +// src/podkop/methods/custom/index.ts +var CustomPodkopMethods = { + getConfigSections, + getDashboardSections +}; + // src/constants.ts var STATUS_COLORS = { SUCCESS: "#4caf50", @@ -538,286 +848,16 @@ var COMMAND_SCHEDULING = { // Lowest priority }; -// src/helpers/executeShellCommand.ts -async function executeShellCommand({ - command, - args, - timeout = COMMAND_TIMEOUT -}) { +// src/podkop/api.ts +async function createBaseApiRequest(fetchFn, options) { + const wrappedFn = () => options?.timeoutMs && options?.operationName ? withTimeout( + fetchFn(), + options.timeoutMs, + options.operationName, + options.timeoutMessage + ) : fetchFn(); try { - return withTimeout( - fs.exec(command, args), - timeout, - [command, ...args].join(" ") - ); - } catch (err) { - const error = err; - return { stdout: "", stderr: error?.message, code: 0 }; - } -} - -// src/helpers/maskIP.ts -function maskIP(ip = "") { - const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`); -} - -// src/helpers/getProxyUrlName.ts -function getProxyUrlName(url) { - try { - const [_link, hash] = url.split("#"); - if (!hash) { - return ""; - } - return decodeURIComponent(hash); - } catch { - return ""; - } -} - -// src/helpers/onMount.ts -async function onMount(id) { - return new Promise((resolve) => { - const el = document.getElementById(id); - if (el && el.offsetParent !== null) { - return resolve(el); - } - const observer = new MutationObserver(() => { - const target = document.getElementById(id); - if (target) { - const io = new IntersectionObserver((entries) => { - const visible = entries.some((e) => e.isIntersecting); - if (visible) { - observer.disconnect(); - io.disconnect(); - resolve(target); - } - }); - io.observe(target); - } - }); - observer.observe(document.body, { - childList: true, - subtree: true - }); - }); -} - -// src/helpers/getClashApiUrl.ts -function getClashApiUrl() { - const { hostname } = window.location; - return `http://${hostname}:9090`; -} -function getClashWsUrl() { - const { hostname } = window.location; - return `ws://${hostname}:9090`; -} -function getClashUIUrl() { - const { hostname } = window.location; - return `http://${hostname}:9090/ui`; -} - -// src/helpers/splitProxyString.ts -function splitProxyString(str) { - return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean); -} - -// src/helpers/preserveScrollForPage.ts -function preserveScrollForPage(renderFn) { - const scrollY = window.scrollY; - renderFn(); - requestAnimationFrame(() => { - window.scrollTo({ top: scrollY }); - }); -} - -// src/helpers/parseQueryString.ts -function parseQueryString(query) { - const clean = query.startsWith("?") ? query.slice(1) : query; - return clean.split("&").filter(Boolean).reduce( - (acc, pair) => { - const [rawKey, rawValue = ""] = pair.split("="); - if (!rawKey) { - return acc; - } - const key = decodeURIComponent(rawKey); - const value = decodeURIComponent(rawValue); - return { ...acc, [key]: value }; - }, - {} - ); -} - -// src/validators/validateVlessUrl.ts -function validateVlessUrl(url) { - try { - if (!url.startsWith("vless://")) - return { - valid: false, - message: "Invalid VLESS URL: must start with vless://" - }; - if (/\s/.test(url)) - return { - valid: false, - message: "Invalid VLESS URL: must not contain spaces" - }; - const body = url.slice("vless://".length); - const [mainPart] = body.split("#"); - const [userHostPort, queryString] = mainPart.split("?"); - if (!userHostPort) - return { - valid: false, - message: "Invalid VLESS URL: missing host and UUID" - }; - const [userPart, hostPortPart] = userHostPort.split("@"); - if (!userPart) - return { valid: false, message: "Invalid VLESS URL: missing UUID" }; - if (!hostPortPart) - return { valid: false, message: "Invalid VLESS URL: missing server" }; - const [host, port] = hostPortPart.split(":"); - if (!host) - return { valid: false, message: "Invalid VLESS URL: missing hostname" }; - if (!port) - return { valid: false, message: "Invalid VLESS URL: missing port" }; - const portNum = Number(port); - if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) - return { - valid: false, - message: "Invalid VLESS URL: invalid port number" - }; - if (!queryString) - return { - valid: false, - message: "Invalid VLESS URL: missing query parameters" - }; - const params = parseQueryString(queryString); - const validTypes = [ - "tcp", - "raw", - "udp", - "grpc", - "http", - "httpupgrade", - "xhttp", - "ws", - "kcp" - ]; - const validSecurities = ["tls", "reality", "none"]; - if (!params.type || !validTypes.includes(params.type)) - return { - valid: false, - message: "Invalid VLESS URL: unsupported or missing type" - }; - if (!params.security || !validSecurities.includes(params.security)) - return { - valid: false, - message: "Invalid VLESS URL: unsupported or missing security" - }; - if (params.security === "reality") { - if (!params.pbk) - return { - valid: false, - message: "Invalid VLESS URL: missing pbk for reality" - }; - if (!params.fp) - return { - valid: false, - message: "Invalid VLESS URL: missing fp for reality" - }; - } - return { valid: true, message: _("Valid") }; - } catch (_e) { - return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; - } -} - -// src/validators/validateOutboundJson.ts -function validateOutboundJson(value) { - try { - const parsed = JSON.parse(value); - if (!parsed.type || !parsed.server || !parsed.server_port) { - return { - valid: false, - message: _( - 'Outbound JSON must contain at least "type", "server" and "server_port" fields' - ) - }; - } - return { valid: true, message: _("Valid") }; - } catch { - return { valid: false, message: _("Invalid JSON format") }; - } -} - -// src/validators/validateTrojanUrl.ts -function validateTrojanUrl(url) { - try { - if (!url.startsWith("trojan://")) { - return { - valid: false, - message: _("Invalid Trojan URL: must start with trojan://") - }; - } - if (!url || /\s/.test(url)) { - return { - valid: false, - message: _("Invalid Trojan URL: must not contain spaces") - }; - } - const body = url.slice("trojan://".length); - const [mainPart] = body.split("#"); - const [userHostPort] = mainPart.split("?"); - const [userPart, hostPortPart] = userHostPort.split("@"); - if (!userHostPort) - return { - valid: false, - message: "Invalid Trojan URL: missing credentials and host" - }; - if (!userPart) - return { valid: false, message: "Invalid Trojan URL: missing password" }; - if (!hostPortPart) - return { - valid: false, - message: "Invalid Trojan URL: missing hostname and port" - }; - const [host, port] = hostPortPart.split(":"); - if (!host) - return { valid: false, message: "Invalid Trojan URL: missing hostname" }; - if (!port) - return { valid: false, message: "Invalid Trojan URL: missing port" }; - const portNum = Number(port); - if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) - return { - valid: false, - message: "Invalid Trojan URL: invalid port number" - }; - } catch (_e) { - return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; - } - return { valid: true, message: _("Valid") }; -} - -// src/validators/validateProxyUrl.ts -function validateProxyUrl(url) { - if (url.startsWith("ss://")) { - return validateShadowsocksUrl(url); - } - if (url.startsWith("vless://")) { - return validateVlessUrl(url); - } - if (url.startsWith("trojan://")) { - return validateTrojanUrl(url); - } - return { - valid: false, - message: _("URL must start with vless:// or ss:// or trojan://") - }; -} - -// src/clash/methods/createBaseApiRequest.ts -async function createBaseApiRequest(fetchFn) { - try { - const response = await fetchFn(); + const response = await wrappedFn(); if (!response.ok) { return { success: false, @@ -837,238 +877,39 @@ async function createBaseApiRequest(fetchFn) { } } -// src/clash/methods/getConfig.ts -async function getClashConfig() { +// src/podkop/methods/fakeip/getFakeIpCheck.ts +async function getFakeIpCheck() { return createBaseApiRequest( - () => fetch(`${getClashApiUrl()}/configs`, { + () => fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { method: "GET", headers: { "Content-Type": "application/json" } - }) - ); -} - -// src/clash/methods/getGroupDelay.ts -async function getClashGroupDelay(group, url = "https://www.gstatic.com/generate_204", timeout = 2e3) { - const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent( - url - )}&timeout=${timeout}`; - return createBaseApiRequest( - () => fetch(endpoint, { - method: "GET", - headers: { "Content-Type": "application/json" } - }) - ); -} - -// src/clash/methods/getProxies.ts -async function getClashProxies() { - return createBaseApiRequest( - () => fetch(`${getClashApiUrl()}/proxies`, { - method: "GET", - headers: { "Content-Type": "application/json" } - }) - ); -} - -// src/clash/methods/getVersion.ts -async function getClashVersion() { - return createBaseApiRequest( - () => fetch(`${getClashApiUrl()}/version`, { - method: "GET", - headers: { "Content-Type": "application/json" } - }) - ); -} - -// src/clash/methods/triggerProxySelector.ts -async function triggerProxySelector(selector, outbound) { - return createBaseApiRequest( - () => fetch(`${getClashApiUrl()}/proxies/${selector}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: outbound }) - }) - ); -} - -// src/clash/methods/triggerLatencyTest.ts -async function triggerLatencyGroupTest(tag, timeout = 5e3, url = "https://www.gstatic.com/generate_204") { - return createBaseApiRequest( - () => fetch( - `${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`, - { - method: "GET", - headers: { "Content-Type": "application/json" } - } - ) - ); -} -async function triggerLatencyProxyTest(tag, timeout = 2e3, url = "https://www.gstatic.com/generate_204") { - return createBaseApiRequest( - () => fetch( - `${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`, - { - method: "GET", - headers: { "Content-Type": "application/json" } - } - ) - ); -} - -// src/podkop/methods/getConfigSections.ts -async function getConfigSections() { - return uci.load("podkop").then(() => uci.sections("podkop")); -} - -// src/podkop/methods/getDashboardSections.ts -async function getDashboardSections() { - const configSections = await getConfigSections(); - const clashProxies = await getClashProxies(); - if (!clashProxies.success) { - return { - success: false, - data: [] - }; - } - const proxies = Object.entries(clashProxies.data.proxies).map( - ([key, value]) => ({ - code: key, - value - }) - ); - const data = configSections.filter((section) => section.mode !== "block").map((section) => { - if (section.mode === "proxy") { - if (section.proxy_config_type === "url") { - const outbound = proxies.find( - (proxy) => proxy.code === `${section[".name"]}-out` - ); - const activeConfigs = splitProxyString(section.proxy_string); - const proxyDisplayName = getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ""; - return { - withTagSelect: false, - code: outbound?.code || section[".name"], - displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || section[".name"], - displayName: proxyDisplayName, - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: true - } - ] - }; - } - if (section.proxy_config_type === "outbound") { - const outbound = proxies.find( - (proxy) => proxy.code === `${section[".name"]}-out` - ); - const parsedOutbound = JSON.parse(section.outbound_json); - const parsedTag = parsedOutbound?.tag ? decodeURIComponent(parsedOutbound?.tag) : void 0; - const proxyDisplayName = parsedTag || outbound?.value?.name || ""; - return { - withTagSelect: false, - code: outbound?.code || section[".name"], - displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || section[".name"], - displayName: proxyDisplayName, - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: true - } - ] - }; - } - if (section.proxy_config_type === "urltest") { - const selector = proxies.find( - (proxy) => proxy.code === `${section[".name"]}-out` - ); - const outbound = proxies.find( - (proxy) => proxy.code === `${section[".name"]}-urltest-out` - ); - const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item, index) => ({ - code: item?.code || "", - displayName: getProxyUrlName(section.urltest_proxy_links?.[index]) || item?.value?.name || "", - latency: item?.value?.history?.[0]?.delay || 0, - type: item?.value?.type || "", - selected: selector?.value?.now === item?.code - })); - return { - withTagSelect: true, - code: selector?.code || section[".name"], - displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || "", - displayName: _("Fastest"), - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: selector?.value?.now === outbound?.code - }, - ...outbounds - ] - }; - } + }), + { + operationName: "getFakeIpCheck", + timeoutMs: 5e3 } - if (section.mode === "vpn") { - const outbound = proxies.find( - (proxy) => proxy.code === `${section[".name"]}-out` - ); - return { - withTagSelect: false, - code: outbound?.code || section[".name"], - displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || section[".name"], - displayName: section.interface || outbound?.value?.name || "", - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: true - } - ] - }; + ); +} + +// src/podkop/methods/fakeip/getIpCheck.ts +async function getIpCheck() { + return createBaseApiRequest( + () => fetch(`https://${IP_CHECK_DOMAIN}/check`, { + method: "GET", + headers: { "Content-Type": "application/json" } + }), + { + operationName: "getIpCheck", + timeoutMs: 5e3 } - return { - withTagSelect: false, - code: section[".name"], - displayName: section[".name"], - outbounds: [] - }; - }); - return { - success: true, - data - }; + ); } -// src/podkop/methods/getPodkopStatus.ts -async function getPodkopStatus() { - const response = await executeShellCommand({ - command: "/usr/bin/podkop", - args: ["get_status"], - timeout: 1e4 - }); - if (response.stdout) { - return JSON.parse(response.stdout.replace(/\n/g, "")); - } - return { enabled: 0, status: "unknown" }; -} - -// src/podkop/methods/getSingboxStatus.ts -async function getSingboxStatus() { - const response = await executeShellCommand({ - command: "/usr/bin/podkop", - args: ["get_sing_box_status"], - timeout: 1e4 - }); - if (response.stdout) { - return JSON.parse(response.stdout.replace(/\n/g, "")); - } - return { running: 0, enabled: 0, status: "unknown" }; -} +// src/podkop/methods/fakeip/index.ts +var RemoteFakeIPMethods = { + getFakeIpCheck, + getIpCheck +}; // src/podkop/services/tab.service.ts var TabService = class _TabService { @@ -1137,7 +978,141 @@ var TabService = class _TabService { }; var TabServiceInstance = TabService.getInstance(); -// src/store.ts +// src/podkop/tabs/diagnostic/checks/contstants.ts +var DIAGNOSTICS_CHECKS_MAP = { + ["DNS" /* DNS */]: { + order: 1, + title: _("DNS checks"), + code: "DNS" /* DNS */ + }, + ["SINGBOX" /* SINGBOX */]: { + order: 2, + title: _("Sing-box checks"), + code: "SINGBOX" /* SINGBOX */ + }, + ["NFT" /* NFT */]: { + order: 3, + title: _("Nftables checks"), + code: "NFT" /* NFT */ + }, + ["FAKEIP" /* FAKEIP */]: { + order: 4, + title: _("FakeIP checks"), + code: "FAKEIP" /* FAKEIP */ + } +}; + +// src/podkop/tabs/diagnostic/diagnostic.store.ts +var initialDiagnosticStore = { + diagnosticsSystemInfo: { + loading: true, + podkop_version: "loading", + podkop_latest_version: "loading", + luci_app_version: "loading", + sing_box_version: "loading", + openwrt_version: "loading", + device_model: "loading" + }, + diagnosticsActions: { + restart: { + loading: false + }, + start: { + loading: false + }, + stop: { + loading: false + }, + enable: { + loading: false + }, + disable: { + loading: false + }, + globalCheck: { + loading: false + }, + viewLogs: { + loading: false + }, + showSingBoxConfig: { + loading: false + } + }, + diagnosticsRunAction: { loading: false }, + diagnosticsChecks: [ + { + code: "DNS" /* DNS */, + title: DIAGNOSTICS_CHECKS_MAP.DNS.title, + order: DIAGNOSTICS_CHECKS_MAP.DNS.order, + description: _("Not running"), + items: [], + state: "skipped" + }, + { + code: "SINGBOX" /* SINGBOX */, + title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title, + order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order, + description: _("Not running"), + items: [], + state: "skipped" + }, + { + code: "NFT" /* NFT */, + title: DIAGNOSTICS_CHECKS_MAP.NFT.title, + order: DIAGNOSTICS_CHECKS_MAP.NFT.order, + description: _("Not running"), + items: [], + state: "skipped" + }, + { + code: "FAKEIP" /* FAKEIP */, + title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title, + order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order, + description: _("Not running"), + items: [], + state: "skipped" + } + ] +}; +var loadingDiagnosticsChecksStore = { + diagnosticsChecks: [ + { + code: "DNS" /* DNS */, + title: DIAGNOSTICS_CHECKS_MAP.DNS.title, + order: DIAGNOSTICS_CHECKS_MAP.DNS.order, + description: _("Queued"), + items: [], + state: "skipped" + }, + { + code: "SINGBOX" /* SINGBOX */, + title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title, + order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order, + description: _("Queued"), + items: [], + state: "skipped" + }, + { + code: "NFT" /* NFT */, + title: DIAGNOSTICS_CHECKS_MAP.NFT.title, + order: DIAGNOSTICS_CHECKS_MAP.NFT.order, + description: _("Queued"), + items: [], + state: "skipped" + }, + { + code: "FAKEIP" /* FAKEIP */, + title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title, + order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order, + description: _("Queued"), + items: [], + state: "skipped" + } + ] +}; + +// src/podkop/services/store.service.ts function jsonStableStringify(obj) { return JSON.stringify(obj, (_2, value) => { if (value && typeof value === "object" && !Array.isArray(value)) { @@ -1159,7 +1134,7 @@ function jsonEqual(a, b) { return false; } } -var Store = class { +var StoreService = class { constructor(initial) { this.listeners = /* @__PURE__ */ new Set(); this.lastHash = ""; @@ -1182,9 +1157,16 @@ var Store = class { } this.listeners.forEach((cb) => cb(this.value, prev, diff)); } - reset() { + reset(keys) { const prev = this.value; - const next = structuredClone(this.initial); + const next = structuredClone(this.value); + if (keys && keys.length > 0) { + keys.forEach((key) => { + next[key] = structuredClone(this.initial[key]); + }); + } else { + Object.assign(next, structuredClone(this.initial)); + } if (jsonEqual(prev, next)) return; this.value = next; this.lastHash = jsonStableStringify(next); @@ -1250,13 +1232,174 @@ var initialStore = { failed: false, latencyFetching: false, data: [] + }, + ...initialDiagnosticStore +}; +var store = new StoreService(initialStore); + +// src/helpers/downloadAsTxt.ts +function downloadAsTxt(text, filename) { + const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + const safeName = filename.endsWith(".txt") ? filename : `${filename}.txt`; + link.download = safeName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); +} + +// src/podkop/services/logger.service.ts +var Logger = class { + constructor() { + this.logs = []; + this.levels = ["debug", "info", "warn", "error"]; + } + format(level, ...args) { + return `[${level.toUpperCase()}] ${args.join(" ")}`; + } + push(level, ...args) { + if (!this.levels.includes(level)) level = "info"; + const message = this.format(level, ...args); + this.logs.push(message); + switch (level) { + case "error": + console.error(message); + break; + case "warn": + console.warn(message); + break; + case "info": + console.info(message); + break; + default: + console.log(message); + } + } + debug(...args) { + this.push("debug", ...args); + } + info(...args) { + this.push("info", ...args); + } + warn(...args) { + this.push("warn", ...args); + } + error(...args) { + this.push("error", ...args); + } + clear() { + this.logs = []; + } + getLogs() { + return this.logs.join("\n"); + } + download(filename = "logs.txt") { + if (typeof document === "undefined") { + console.warn("Logger.download() \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435"); + return; + } + downloadAsTxt(this.getLogs(), filename); + } +}; +var logger = new Logger(); + +// src/podkop/services/podkopLogWatcher.service.ts +var PodkopLogWatcher = class _PodkopLogWatcher { + constructor() { + this.intervalMs = 5e3; + this.lastLines = /* @__PURE__ */ new Set(); + this.running = false; + this.paused = false; + if (typeof document !== "undefined") { + document.addEventListener("visibilitychange", () => { + if (document.hidden) this.pause(); + else this.resume(); + }); + } + } + static getInstance() { + if (!_PodkopLogWatcher.instance) { + _PodkopLogWatcher.instance = new _PodkopLogWatcher(); + } + return _PodkopLogWatcher.instance; + } + init(fetcher, options) { + this.fetcher = fetcher; + this.onNewLog = options?.onNewLog; + this.intervalMs = options?.intervalMs ?? 5e3; + logger.info( + "[PodkopLogWatcher]", + `initialized (interval: ${this.intervalMs}ms)` + ); + } + async checkOnce() { + if (!this.fetcher) { + logger.warn("[PodkopLogWatcher]", "fetcher not found"); + return; + } + if (this.paused) { + logger.debug("[PodkopLogWatcher]", "skipped check \u2014 tab not visible"); + return; + } + try { + const raw = await this.fetcher(); + const lines = raw.split("\n").filter(Boolean); + for (const line of lines) { + if (!this.lastLines.has(line)) { + this.lastLines.add(line); + this.onNewLog?.(line); + } + } + if (this.lastLines.size > 500) { + const arr = Array.from(this.lastLines); + this.lastLines = new Set(arr.slice(-500)); + } + } catch (err) { + logger.error("[PodkopLogWatcher]", "failed to read logs:", err); + } + } + start() { + if (this.running) return; + if (!this.fetcher) { + logger.warn("[PodkopLogWatcher]", "attempted to start without fetcher"); + return; + } + this.running = true; + this.timer = setInterval(() => this.checkOnce(), this.intervalMs); + logger.info( + "[PodkopLogWatcher]", + `started (interval: ${this.intervalMs}ms)` + ); + } + stop() { + if (!this.running) return; + this.running = false; + if (this.timer) clearInterval(this.timer); + logger.info("[PodkopLogWatcher]", "stopped"); + } + pause() { + if (!this.running || this.paused) return; + this.paused = true; + logger.info("[PodkopLogWatcher]", "paused (tab not visible)"); + } + resume() { + if (!this.running || !this.paused) return; + this.paused = false; + logger.info("[PodkopLogWatcher]", "resumed (tab active)"); + this.checkOnce(); + } + reset() { + this.lastLines.clear(); + logger.info("[PodkopLogWatcher]", "log history reset"); } }; -var store = new Store(initialStore); // src/podkop/services/core.service.ts function coreService() { TabServiceInstance.onChange((activeId, tabs) => { + logger.info("[TAB]", activeId); store.set({ tabService: { current: activeId || "", @@ -1264,9 +1407,167 @@ function coreService() { } }); }); + const watcher = PodkopLogWatcher.getInstance(); + watcher.init( + async () => { + const logs = await PodkopShellMethods.checkLogs(); + if (logs.success) { + return logs.data; + } + return ""; + }, + { + intervalMs: 3e3, + onNewLog: (line) => { + if (line.toLowerCase().includes("[error]") || line.toLowerCase().includes("[fatal]")) { + ui.addNotification("Podkop Error", E("div", {}, line), "error"); + } + } + } + ); + watcher.start(); } -// src/podkop/tabs/dashboard/renderSections.ts +// src/podkop/services/socket.service.ts +var SocketManager = class _SocketManager { + constructor() { + this.sockets = /* @__PURE__ */ new Map(); + this.listeners = /* @__PURE__ */ new Map(); + this.connected = /* @__PURE__ */ new Map(); + this.errorListeners = /* @__PURE__ */ new Map(); + } + static getInstance() { + if (!_SocketManager.instance) { + _SocketManager.instance = new _SocketManager(); + } + return _SocketManager.instance; + } + resetAll() { + for (const [url, ws] of this.sockets.entries()) { + try { + if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { + ws.close(); + } + } catch (err) { + logger.error( + "[SOCKET]", + `resetAll: failed to close socket ${url}`, + err + ); + } + } + this.sockets.clear(); + this.listeners.clear(); + this.errorListeners.clear(); + this.connected.clear(); + logger.info("[SOCKET]", "All connections and state have been reset."); + } + connect(url) { + if (this.sockets.has(url)) return; + let ws; + try { + ws = new WebSocket(url); + } catch (err) { + logger.error( + "[SOCKET]", + `failed to construct WebSocket for ${url}:`, + err + ); + this.triggerError(url, err instanceof Event ? err : String(err)); + return; + } + this.sockets.set(url, ws); + this.connected.set(url, false); + this.listeners.set(url, /* @__PURE__ */ new Set()); + this.errorListeners.set(url, /* @__PURE__ */ new Set()); + ws.addEventListener("open", () => { + this.connected.set(url, true); + logger.info("[SOCKET]", "Connected to", url); + }); + ws.addEventListener("message", (event) => { + const handlers = this.listeners.get(url); + if (handlers) { + for (const handler of handlers) { + try { + handler(event.data); + } catch (err) { + logger.error("[SOCKET]", `Handler error for ${url}:`, err); + } + } + } + }); + ws.addEventListener("close", () => { + this.connected.set(url, false); + logger.warn("[SOCKET]", `Disconnected: ${url}`); + this.triggerError(url, "Connection closed"); + }); + ws.addEventListener("error", (err) => { + logger.error("[SOCKET]", `Socket error for ${url}:`, err); + this.triggerError(url, err); + }); + } + subscribe(url, listener, onError) { + if (!this.errorListeners.has(url)) { + this.errorListeners.set(url, /* @__PURE__ */ new Set()); + } + if (onError) { + this.errorListeners.get(url)?.add(onError); + } + if (!this.sockets.has(url)) { + this.connect(url); + } + if (!this.listeners.has(url)) { + this.listeners.set(url, /* @__PURE__ */ new Set()); + } + this.listeners.get(url)?.add(listener); + } + unsubscribe(url, listener, onError) { + this.listeners.get(url)?.delete(listener); + if (onError) { + this.errorListeners.get(url)?.delete(onError); + } + } + // eslint-disable-next-line + send(url, data) { + const ws = this.sockets.get(url); + if (ws && this.connected.get(url)) { + ws.send(typeof data === "string" ? data : JSON.stringify(data)); + } else { + logger.warn("[SOCKET]", `Cannot send: not connected to ${url}`); + this.triggerError(url, "Not connected"); + } + } + disconnect(url) { + const ws = this.sockets.get(url); + if (ws) { + ws.close(); + this.sockets.delete(url); + this.listeners.delete(url); + this.errorListeners.delete(url); + this.connected.delete(url); + } + } + disconnectAll() { + for (const url of this.sockets.keys()) { + this.disconnect(url); + } + } + triggerError(url, err) { + const handlers = this.errorListeners.get(url); + if (handlers) { + for (const cb of handlers) { + try { + cb(err); + } catch (e) { + logger.error("[SOCKET]", `Error handler threw for ${url}:`, e); + } + } + } + } +}; +var socket = SocketManager.getInstance(); + +// src/podkop/tabs/dashboard/partials/renderSections.ts function renderFailedState() { return E( "div", @@ -1274,10 +1575,7 @@ function renderFailedState() { class: "pdk_dashboard-page__outbound-section centered", style: "height: 127px" }, - E("span", {}, [ - E("span", {}, _("Dashboard currently unavailable")), - E("div", { style: "text-align: center;" }, `API: ${getClashApiUrl()}`) - ]) + E("span", {}, [E("span", {}, _("Dashboard currently unavailable"))]) ); } function renderLoadingState() { @@ -1373,7 +1671,7 @@ function renderSections(props) { return renderDefaultState(props); } -// src/podkop/tabs/dashboard/renderWidget.ts +// src/podkop/tabs/dashboard/partials/renderWidget.ts function renderFailedState2() { return E( "div", @@ -1435,8 +1733,8 @@ function renderWidget(props) { return renderDefaultState2(props); } -// src/podkop/tabs/dashboard/renderDashboard.ts -function renderDashboard() { +// src/podkop/tabs/dashboard/render.ts +function render() { return E( "div", { @@ -1483,115 +1781,14 @@ function renderDashboard() { onTestLatency: () => { }, onChooseOutbound: () => { - } + }, + latencyFetching: false }) ) ] ); } -// src/socket.ts -var SocketManager = class _SocketManager { - constructor() { - this.sockets = /* @__PURE__ */ new Map(); - this.listeners = /* @__PURE__ */ new Map(); - this.connected = /* @__PURE__ */ new Map(); - this.errorListeners = /* @__PURE__ */ new Map(); - } - static getInstance() { - if (!_SocketManager.instance) { - _SocketManager.instance = new _SocketManager(); - } - return _SocketManager.instance; - } - connect(url) { - if (this.sockets.has(url)) return; - const ws = new WebSocket(url); - this.sockets.set(url, ws); - this.connected.set(url, false); - this.listeners.set(url, /* @__PURE__ */ new Set()); - this.errorListeners.set(url, /* @__PURE__ */ new Set()); - ws.addEventListener("open", () => { - this.connected.set(url, true); - console.info(`Connected: ${url}`); - }); - ws.addEventListener("message", (event) => { - const handlers = this.listeners.get(url); - if (handlers) { - for (const handler of handlers) { - try { - handler(event.data); - } catch (err) { - console.error(`Handler error for ${url}:`, err); - } - } - } - }); - ws.addEventListener("close", () => { - this.connected.set(url, false); - console.warn(`Disconnected: ${url}`); - this.triggerError(url, "Connection closed"); - }); - ws.addEventListener("error", (err) => { - console.error(`Socket error for ${url}:`, err); - this.triggerError(url, err); - }); - } - subscribe(url, listener, onError) { - if (!this.sockets.has(url)) { - this.connect(url); - } - this.listeners.get(url)?.add(listener); - if (onError) { - this.errorListeners.get(url)?.add(onError); - } - } - unsubscribe(url, listener, onError) { - this.listeners.get(url)?.delete(listener); - if (onError) { - this.errorListeners.get(url)?.delete(onError); - } - } - // eslint-disable-next-line - send(url, data) { - const ws = this.sockets.get(url); - if (ws && this.connected.get(url)) { - ws.send(typeof data === "string" ? data : JSON.stringify(data)); - } else { - console.warn(`Cannot send: not connected to ${url}`); - this.triggerError(url, "Not connected"); - } - } - disconnect(url) { - const ws = this.sockets.get(url); - if (ws) { - ws.close(); - this.sockets.delete(url); - this.listeners.delete(url); - this.errorListeners.delete(url); - this.connected.delete(url); - } - } - disconnectAll() { - for (const url of this.sockets.keys()) { - this.disconnect(url); - } - } - triggerError(url, err) { - const handlers = this.errorListeners.get(url); - if (handlers) { - for (const cb of handlers) { - try { - cb(err); - } catch (e) { - console.error(`Error handler threw for ${url}:`, e); - } - } - } - } -}; -var socket = SocketManager.getInstance(); - // src/helpers/prettyBytes.ts function prettyBytes(n) { const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; @@ -1604,7 +1801,33 @@ function prettyBytes(n) { return n + " " + unit; } -// src/podkop/tabs/dashboard/initDashboardController.ts +// src/podkop/fetchers/fetchServicesInfo.ts +async function fetchServicesInfo() { + const [podkop, singbox] = await Promise.all([ + PodkopShellMethods.getStatus(), + PodkopShellMethods.getSingBoxStatus() + ]); + if (!podkop.success || !singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, podkop: 0 } + } + }); + } + if (podkop.success && singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.data.running, podkop: podkop.data.enabled } + } + }); + } +} + +// src/podkop/tabs/dashboard/initController.ts async function fetchDashboardSections() { const prev = store.get().sectionsWidget; store.set({ @@ -1613,9 +1836,9 @@ async function fetchDashboardSections() { failed: false } }); - const { data, success } = await getDashboardSections(); + const { data, success } = await CustomPodkopMethods.getDashboardSections(); if (!success) { - console.log("[fetchDashboardSections]: failed to fetch", getClashApiUrl()); + logger.error("[DASHBOARD]", "fetchDashboardSections: failed to fetch"); } store.set({ sectionsWidget: { @@ -1626,30 +1849,6 @@ async function fetchDashboardSections() { } }); } -async function fetchServicesInfo() { - try { - const [podkop, singbox] = await Promise.all([ - getPodkopStatus(), - getSingboxStatus() - ]); - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { singbox: singbox.running, podkop: podkop.enabled } - } - }); - } catch (err) { - console.log("[fetchServicesInfo]: failed to fetchServices", err); - store.set({ - servicesInfoWidget: { - loading: false, - failed: true, - data: { singbox: 0, podkop: 0 } - } - }); - } -} async function connectToClashSockets() { socket.subscribe( `${getClashWsUrl()}/traffic?token=`, @@ -1664,8 +1863,9 @@ async function connectToClashSockets() { }); }, (_err) => { - console.log( - "[fetchDashboardSections]: failed to connect", + logger.error( + "[DASHBOARD]", + "connectToClashSockets - traffic: failed to connect to", getClashWsUrl() ); store.set({ @@ -1701,8 +1901,9 @@ async function connectToClashSockets() { }); }, (_err) => { - console.log( - "[fetchDashboardSections]: failed to connect", + logger.error( + "[DASHBOARD]", + "connectToClashSockets - connections: failed to connect to", getClashWsUrl() ); store.set({ @@ -1724,7 +1925,7 @@ async function connectToClashSockets() { ); } async function handleChooseOutbound(selector, tag) { - await triggerProxySelector(selector, tag); + await PodkopShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections(); } async function handleTestGroupLatency(tag) { @@ -1734,7 +1935,7 @@ async function handleTestGroupLatency(tag) { latencyFetching: true } }); - await triggerLatencyGroupTest(tag); + await PodkopShellMethods.getClashApiGroupLatency(tag); await fetchDashboardSections(); store.set({ sectionsWidget: { @@ -1750,7 +1951,7 @@ async function handleTestProxyLatency(tag) { latencyFetching: true } }); - await triggerLatencyProxyTest(tag); + await PodkopShellMethods.getClashApiProxyLatency(tag); await fetchDashboardSections(); store.set({ sectionsWidget: { @@ -1760,7 +1961,7 @@ async function handleTestProxyLatency(tag) { }); } async function renderSectionsWidget() { - console.log("renderSectionsWidget"); + logger.debug("[DASHBOARD]", "renderSectionsWidget"); const sectionsWidget = store.get().sectionsWidget; const container = document.getElementById("dashboard-sections-grid"); if (sectionsWidget.loading || sectionsWidget.failed) { @@ -1805,7 +2006,7 @@ async function renderSectionsWidget() { }); } async function renderBandwidthWidget() { - console.log("renderBandwidthWidget"); + logger.debug("[DASHBOARD]", "renderBandwidthWidget"); const traffic = store.get().bandwidthWidget; const container = document.getElementById("dashboard-widget-traffic"); if (traffic.loading || traffic.failed) { @@ -1829,7 +2030,7 @@ async function renderBandwidthWidget() { container.replaceChildren(renderedWidget); } async function renderTrafficTotalWidget() { - console.log("renderTrafficTotalWidget"); + logger.debug("[DASHBOARD]", "renderTrafficTotalWidget"); const trafficTotalWidget = store.get().trafficTotalWidget; const container = document.getElementById("dashboard-widget-traffic-total"); if (trafficTotalWidget.loading || trafficTotalWidget.failed) { @@ -1859,7 +2060,7 @@ async function renderTrafficTotalWidget() { container.replaceChildren(renderedWidget); } async function renderSystemInfoWidget() { - console.log("renderSystemInfoWidget"); + logger.debug("[DASHBOARD]", "renderSystemInfoWidget"); const systemInfoWidget = store.get().systemInfoWidget; const container = document.getElementById("dashboard-widget-system-info"); if (systemInfoWidget.loading || systemInfoWidget.failed) { @@ -1889,7 +2090,7 @@ async function renderSystemInfoWidget() { container.replaceChildren(renderedWidget); } async function renderServicesInfoWidget() { - console.log("renderServicesInfoWidget"); + logger.debug("[DASHBOARD]", "renderServicesInfoWidget"); const servicesInfoWidget = store.get().servicesInfoWidget; const container = document.getElementById("dashboard-widget-service-info"); if (servicesInfoWidget.loading || servicesInfoWidget.failed) { @@ -1941,16 +2142,2372 @@ async function onStoreUpdate(next, prev, diff) { renderServicesInfoWidget(); } } -async function initDashboardController() { - onMount("dashboard-status").then(() => { - store.unsubscribe(onStoreUpdate); - store.reset(); - store.subscribe(onStoreUpdate); - fetchDashboardSections(); - fetchServicesInfo(); - connectToClashSockets(); +async function onPageMount() { + onPageUnmount(); + store.subscribe(onStoreUpdate); + await fetchDashboardSections(); + await fetchServicesInfo(); + await connectToClashSockets(); +} +function onPageUnmount() { + store.unsubscribe(onStoreUpdate); + store.reset([ + "bandwidthWidget", + "trafficTotalWidget", + "systemInfoWidget", + "servicesInfoWidget", + "sectionsWidget" + ]); + socket.resetAll(); +} +function registerLifecycleListeners() { + store.subscribe((next, prev, diff) => { + if (diff.tabService && next.tabService.current !== prev.tabService.current) { + logger.debug( + "[DASHBOARD]", + "active tab diff event, active tab:", + diff.tabService.current + ); + const isDashboardVisible = next.tabService.current === "dashboard"; + if (isDashboardVisible) { + logger.debug( + "[DASHBOARD]", + "registerLifecycleListeners", + "onPageMount" + ); + return onPageMount(); + } + if (!isDashboardVisible) { + logger.debug( + "[DASHBOARD]", + "registerLifecycleListeners", + "onPageUnmount" + ); + return onPageUnmount(); + } + } }); } +async function initController() { + onMount("dashboard-status").then(() => { + logger.debug("[DASHBOARD]", "initController", "onMount"); + onPageMount(); + registerLifecycleListeners(); + }); +} + +// src/podkop/tabs/dashboard/styles.ts +var styles = ` +#cbi-podkop-dashboard-_mount_node > div { + width: 100%; +} + +#cbi-podkop-dashboard > h3 { + display: none; +} + +.pdk_dashboard-page { + width: 100%; + --dashboard-grid-columns: 4; +} + +@media (max-width: 900px) { + .pdk_dashboard-page { + --dashboard-grid-columns: 2; + } +} + +.pdk_dashboard-page__widgets-section { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; +} + +.pdk_dashboard-page__widgets-section__item { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; +} + +.pdk_dashboard-page__widgets-section__item__title {} + +.pdk_dashboard-page__widgets-section__item__row {} + +.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--error-color-medium, red); +} + +.pdk_dashboard-page__widgets-section__item__row__key {} + +.pdk_dashboard-page__widgets-section__item__row__value {} + +.pdk_dashboard-page__outbound-section { + margin-top: 10px; + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; +} + +.pdk_dashboard-page__outbound-section__title-section { + display: flex; + align-items: center; + justify-content: space-between; +} + +.pdk_dashboard-page__outbound-section__title-section__title { + color: var(--text-color-high); + font-weight: 700; +} + +.pdk_dashboard-page__outbound-grid { + margin-top: 5px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; +} + +.pdk_dashboard-page__outbound-grid__item { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + transition: border 0.2s ease; +} + +.pdk_dashboard-page__outbound-grid__item--selectable { + cursor: pointer; +} + +.pdk_dashboard-page__outbound-grid__item--selectable:hover { + border-color: var(--primary-color-high, dodgerblue); +} + +.pdk_dashboard-page__outbound-grid__item--active { + border-color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__outbound-grid__item__footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; +} + +.pdk_dashboard-page__outbound-grid__item__type {} + +.pdk_dashboard-page__outbound-grid__item__latency--empty { + color: var(--primary-color-low, lightgray); +} + +.pdk_dashboard-page__outbound-grid__item__latency--green { + color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__outbound-grid__item__latency--yellow { + color: var(--warn-color-medium, orange); +} + +.pdk_dashboard-page__outbound-grid__item__latency--red { + color: var(--error-color-medium, red); +} + +`; + +// src/podkop/tabs/dashboard/index.ts +var DashboardTab = { + render, + initController, + styles +}; + +// src/podkop/tabs/diagnostic/renderDiagnostic.ts +function render2() { + return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [ + E("div", { class: "pdk_diagnostic-page__left-bar" }, [ + E("div", { id: "pdk_diagnostic-page-run-check" }), + E("div", { + class: "pdk_diagnostic-page__checks", + id: "pdk_diagnostic-page-checks" + }) + ]), + E("div", { class: "pdk_diagnostic-page__right-bar" }, [ + E("div", { id: "pdk_diagnostic-page-actions" }), + E("div", { id: "pdk_diagnostic-page-system-info" }) + ]) + ]); +} + +// src/podkop/tabs/diagnostic/checks/updateCheckStore.ts +function updateCheckStore(check, minified) { + const diagnosticsChecks = store.get().diagnosticsChecks; + const other = diagnosticsChecks.filter((item) => item.code !== check.code); + const smallCheck = { + ...check, + items: check.items.filter((item) => item.state !== "success") + }; + const targetCheck = minified ? smallCheck : check; + store.set({ + diagnosticsChecks: [...other, targetCheck] + }); +} + +// src/podkop/tabs/diagnostic/checks/runDnsCheck.ts +async function runDnsCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; + updateCheckStore({ + order, + code, + title, + description: _("Checking dns, please wait"), + state: "loading", + items: [] + }); + const dnsChecks = await PodkopShellMethods.checkDNSAvailable(); + if (!dnsChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive DNS checks result"), + state: "error", + items: [] + }); + throw new Error("DNS checks failed"); + } + const data = dnsChecks.data; + const allGood = Boolean(data.dns_on_router) && Boolean(data.dhcp_config_status) && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status); + const atLeastOneGood = Boolean(data.dns_on_router) || Boolean(data.dhcp_config_status) || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status); + function getStatus() { + if (allGood) { + return "success"; + } + if (atLeastOneGood) { + return "warning"; + } + return "error"; + } + updateCheckStore({ + order, + code, + title, + description: _("DNS checks passed"), + state: getStatus(), + items: [ + ...insertIf( + data.dns_type === "doh" || data.dns_type === "dot", + [ + { + state: data.bootstrap_dns_status ? "success" : "error", + key: _("Bootsrap DNS"), + value: data.bootstrap_dns_server + } + ] + ), + { + state: data.dns_status ? "success" : "error", + key: _("Main DNS"), + value: `${data.dns_server} [${data.dns_type}]` + }, + { + state: data.dns_on_router ? "success" : "error", + key: _("DNS on router"), + value: "" + }, + { + state: data.dhcp_config_status ? "success" : "error", + key: _("DHCP has DNS server"), + value: "" + } + ] + }); + if (!atLeastOneGood) { + throw new Error("DNS checks failed"); + } +} + +// src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts +async function runSingBoxCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; + updateCheckStore({ + order, + code, + title, + description: _("Checking sing-box, please wait"), + state: "loading", + items: [] + }); + const singBoxChecks = await PodkopShellMethods.checkSingBox(); + if (!singBoxChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive Sing-box checks result"), + state: "error", + items: [] + }); + throw new Error("Sing-box checks failed"); + } + const data = singBoxChecks.data; + const allGood = Boolean(data.sing_box_installed) && Boolean(data.sing_box_version_ok) && Boolean(data.sing_box_service_exist) && Boolean(data.sing_box_autostart_disabled) && Boolean(data.sing_box_process_running) && Boolean(data.sing_box_ports_listening); + const atLeastOneGood = Boolean(data.sing_box_installed) || Boolean(data.sing_box_version_ok) || Boolean(data.sing_box_service_exist) || Boolean(data.sing_box_autostart_disabled) || Boolean(data.sing_box_process_running) || Boolean(data.sing_box_ports_listening); + function getStatus() { + if (allGood) { + return "success"; + } + if (atLeastOneGood) { + return "warning"; + } + return "error"; + } + updateCheckStore({ + order, + code, + title, + description: _("Sing-box checks passed"), + state: getStatus(), + items: [ + { + state: data.sing_box_installed ? "success" : "error", + key: _("Sing-box installed"), + value: "" + }, + { + state: data.sing_box_version_ok ? "success" : "error", + key: _("Sing-box version >= 1.12.4"), + value: "" + }, + { + state: data.sing_box_service_exist ? "success" : "error", + key: _("Sing-box service exist"), + value: "" + }, + { + state: data.sing_box_autostart_disabled ? "success" : "error", + key: _("Sing-box autostart disabled"), + value: "" + }, + { + state: data.sing_box_process_running ? "success" : "error", + key: _("Sing-box process running"), + value: "" + }, + { + state: data.sing_box_ports_listening ? "success" : "error", + key: _("Sing-box listening ports"), + value: "" + } + ] + }); + if (!atLeastOneGood || !data.sing_box_process_running) { + throw new Error("Sing-box checks failed"); + } +} + +// src/podkop/tabs/diagnostic/checks/runNftCheck.ts +async function runNftCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; + updateCheckStore({ + order, + code, + title, + description: _("Checking nftables, please wait"), + state: "loading", + items: [] + }); + await RemoteFakeIPMethods.getFakeIpCheck(); + await RemoteFakeIPMethods.getIpCheck(); + const nftablesChecks = await PodkopShellMethods.checkNftRules(); + if (!nftablesChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive nftables checks result"), + state: "error", + items: [] + }); + throw new Error("Nftables checks failed"); + } + const data = nftablesChecks.data; + const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_mangle_output_counters) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; + const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_mangle_output_counters) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; + function getStatus() { + if (allGood) { + return "success"; + } + if (atLeastOneGood) { + return "warning"; + } + return "error"; + } + updateCheckStore({ + order, + code, + title, + description: allGood ? _("Nftables checks passed") : _("Nftables checks partially passed"), + state: getStatus(), + items: [ + { + state: data.table_exist ? "success" : "error", + key: _("Table exist"), + value: "" + }, + { + state: data.rules_mangle_exist ? "success" : "error", + key: _("Rules mangle exist"), + value: "" + }, + { + state: data.rules_mangle_counters ? "success" : "error", + key: _("Rules mangle counters"), + value: "" + }, + { + state: data.rules_mangle_output_exist ? "success" : "error", + key: _("Rules mangle output exist"), + value: "" + }, + { + state: data.rules_mangle_output_counters ? "success" : "error", + key: _("Rules mangle output counters"), + value: "" + }, + { + state: data.rules_proxy_exist ? "success" : "error", + key: _("Rules proxy exist"), + value: "" + }, + { + state: data.rules_proxy_counters ? "success" : "error", + key: _("Rules proxy counters"), + value: "" + }, + { + state: !data.rules_other_mark_exist ? "success" : "warning", + key: !data.rules_other_mark_exist ? _("No other marking rules found") : _("Additional marking rules found"), + value: "" + } + ] + }); + if (!atLeastOneGood) { + throw new Error("Nftables checks failed"); + } +} + +// src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts +async function runFakeIPCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; + updateCheckStore({ + order, + code, + title, + description: _("Checking FakeIP, please wait"), + state: "loading", + items: [] + }); + const routerFakeIPResponse = await PodkopShellMethods.checkFakeIP(); + const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); + const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); + const checks = { + router: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, + browserFakeIP: checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, + differentIP: checkFakeIPResponse.success && checkIPResponse.success && checkFakeIPResponse.data.IP !== checkIPResponse.data.IP + }; + const allGood = checks.router || checks.browserFakeIP || checks.differentIP; + const atLeastOneGood = checks.router && checks.browserFakeIP && checks.differentIP; + function getMeta() { + if (allGood) { + return { + state: "success", + description: _("FakeIP checks passed") + }; + } + if (atLeastOneGood) { + return { + state: "warning", + description: _("FakeIP checks partially passed") + }; + } + return { + state: "error", + description: _("FakeIP checks failed") + }; + } + const { state, description } = getMeta(); + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + { + state: checks.router ? "success" : "warning", + key: checks.router ? _("Router DNS is routed through sing-box") : _("Router DNS is not routed through sing-box"), + value: "" + }, + { + state: checks.browserFakeIP ? "success" : "error", + key: checks.browserFakeIP ? _("Browser is using FakeIP correctly") : _("Browser is not using FakeIP"), + value: "" + }, + ...insertIf(checks.browserFakeIP, [ + { + state: checks.differentIP ? "success" : "error", + key: checks.differentIP ? _("Proxy traffic is routed via FakeIP") : _("Proxy traffic is not routed via FakeIP"), + value: "" + } + ]) + ] + }); +} + +// src/partials/button/styles.ts +var styles2 = ` +.pdk-partial-button { + text-align: center; +} + +.pdk-partial-button--with-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button--loading { +} + +.pdk-partial-button--disabled { +} + +.pdk-partial-button__icon { + margin-right: 5px; +} + +.pdk-partial-button__icon { + display: flex; + align-items: center; + justify-content: center; +} + +.pdk-partial-button__icon svg { + width: 16px; + height: 16px; +} +`; + +// src/partials/modal/styles.ts +var styles3 = ` + +.pdk-partial-modal__body {} + +.pdk-partial-modal__content { + max-height: 70vh; + overflow: scroll; + border-radius: 4px; +} + +.pdk-partial-modal__footer { + display: flex; + justify-content: flex-end; +} + +.pdk-partial-modal__footer button { + margin-left: 10px; +} +`; + +// src/icons/renderLoaderCircleIcon24.ts +function renderLoaderCircleIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-loader-circle rotate" + }, + [ + svgEl("path", { + d: "M21 12a9 9 0 1 1-6.219-8.56" + }), + svgEl("animateTransform", { + attributeName: "transform", + attributeType: "XML", + type: "rotate", + from: "0 12 12", + to: "360 12 12", + dur: "1s", + repeatCount: "indefinite" + }) + ] + ); +} + +// src/icons/renderCircleAlertIcon24.ts +function renderCircleAlertIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-alert-icon lucide-circle-alert" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("line", { + x1: "12", + y1: "8", + x2: "12", + y2: "12" + }), + svgEl("line", { + x1: "12", + y1: "16", + x2: "12.01", + y2: "16" + }) + ] + ); +} + +// src/icons/renderCircleCheckIcon24.ts +function renderCircleCheckIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-check-icon lucide-circle-check" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("path", { + d: "M9 12l2 2 4-4" + }) + ] + ); +} + +// src/icons/renderCircleSlashIcon24.ts +function renderCircleSlashIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-slash-icon lucide-circle-slash" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("line", { + x1: "9", + y1: "15", + x2: "15", + y2: "9" + }) + ] + ); +} + +// src/icons/renderCircleXIcon24.ts +function renderCircleXIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-x-icon lucide-circle-x" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("path", { + d: "M15 9L9 15" + }), + svgEl("path", { + d: "M9 9L15 15" + }) + ] + ); +} + +// src/icons/renderCheckIcon24.ts +function renderCheckIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-check-icon lucide-check" + }, + [ + svgEl("path", { + d: "M20 6 9 17l-5-5" + }) + ] + ); +} + +// src/icons/renderXIcon24.ts +function renderXIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-x-icon lucide-x" + }, + [svgEl("path", { d: "M18 6 6 18" }), svgEl("path", { d: "m6 6 12 12" })] + ); +} + +// src/icons/renderTriangleAlertIcon24.ts +function renderTriangleAlertIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-triangle-alert-icon lucide-triangle-alert" + }, + [ + svgEl("path", { + d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" + }), + svgEl("path", { d: "M12 9v4" }), + svgEl("path", { d: "M12 17h.01" }) + ] + ); +} + +// src/icons/renderPauseIcon24.ts +function renderPauseIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-pause-icon lucide-pause" + }, + [ + svgEl("rect", { + x: "14", + y: "3", + width: "5", + height: "18", + rx: "1" + }), + svgEl("rect", { + x: "5", + y: "3", + width: "5", + height: "18", + rx: "1" + }) + ] + ); +} + +// src/icons/renderPlayIcon24.ts +function renderPlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-play-icon lucide-play" + }, + [ + svgEl("path", { + d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" + }) + ] + ); +} + +// src/icons/renderRotateCcwIcon24.ts +function renderRotateCcwIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" + }, + [ + svgEl("path", { + d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" + }), + svgEl("path", { + d: "M3 3v5h5" + }) + ] + ); +} + +// src/icons/renderCircleStopIcon24.ts +function renderCircleStopIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-stop-icon lucide-circle-stop" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("rect", { + x: "9", + y: "9", + width: "6", + height: "6", + rx: "1" + }) + ] + ); +} + +// src/icons/renderCirclePlayIcon24.ts +function renderCirclePlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-play-icon lucide-circle-play" + }, + [ + svgEl("path", { + d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" + }), + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }) + ] + ); +} + +// src/icons/renderCircleCheckBigIcon24.ts +function renderCircleCheckBigIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" + }, + [ + svgEl("path", { + d: "M21.801 10A10 10 0 1 1 17 3.335" + }), + svgEl("path", { + d: "m9 11 3 3L22 4" + }) + ] + ); +} + +// src/icons/renderSquareChartGanttIcon24.ts +function renderSquareChartGanttIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" + }, + [ + svgEl("rect", { + width: "18", + height: "18", + x: "3", + y: "3", + rx: "2" + }), + svgEl("path", { d: "M9 8h7" }), + svgEl("path", { d: "M8 12h6" }), + svgEl("path", { d: "M11 16h5" }) + ] + ); +} + +// src/icons/renderCogIcon24.ts +function renderCogIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-cog-icon lucide-cog" + }, + [ + svgEl("path", { d: "M11 10.27 7 3.34" }), + svgEl("path", { d: "m11 13.73-4 6.93" }), + svgEl("path", { d: "M12 22v-2" }), + svgEl("path", { d: "M12 2v2" }), + svgEl("path", { d: "M14 12h8" }), + svgEl("path", { d: "m17 20.66-1-1.73" }), + svgEl("path", { d: "m17 3.34-1 1.73" }), + svgEl("path", { d: "M2 12h2" }), + svgEl("path", { d: "m20.66 17-1.73-1" }), + svgEl("path", { d: "m20.66 7-1.73 1" }), + svgEl("path", { d: "m3.34 17 1.73-1" }), + svgEl("path", { d: "m3.34 7 1.73 1" }), + svgEl("circle", { cx: "12", cy: "12", r: "2" }), + svgEl("circle", { cx: "12", cy: "12", r: "8" }) + ] + ); +} + +// src/icons/renderSearchIcon24.ts +function renderSearchIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-search-icon lucide-search" + }, + [ + svgEl("path", { d: "m21 21-4.34-4.34" }), + svgEl("circle", { cx: "11", cy: "11", r: "8" }) + ] + ); +} + +// src/partials/button/renderButton.ts +function renderButton({ + classNames = [], + disabled, + loading, + onClick, + text, + icon +}) { + const hasIcon = !!loading || !!icon; + function getWrappedIcon() { + const iconWrap = E("span", { + class: "pdk-partial-button__icon" + }); + if (loading) { + iconWrap.appendChild(renderLoaderCircleIcon24()); + return iconWrap; + } + if (icon) { + iconWrap.appendChild(icon()); + return iconWrap; + } + return iconWrap; + } + function getClass() { + return [ + "btn", + "pdk-partial-button", + ...insertIf(Boolean(disabled), ["pdk-partial-button--disabled"]), + ...insertIf(Boolean(loading), ["pdk-partial-button--loading"]), + ...insertIf(Boolean(hasIcon), ["pdk-partial-button--with-icon"]), + ...classNames + ].filter(Boolean).join(" "); + } + function getDisabled() { + if (loading || disabled) { + return true; + } + return void 0; + } + return E( + "button", + { class: getClass(), disabled: getDisabled(), click: onClick }, + [...insertIf(hasIcon, [getWrappedIcon()]), E("span", {}, text)] + ); +} + +// src/helpers/showToast.ts +function showToast(message, type, duration = 3e3) { + let container = document.querySelector(".toast-container"); + if (!container) { + container = document.createElement("div"); + container.className = "toast-container"; + document.body.appendChild(container); + } + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + container.appendChild(toast); + setTimeout(() => toast.classList.add("visible"), 100); + setTimeout(() => { + toast.classList.remove("visible"); + setTimeout(() => toast.remove(), 300); + }, duration); +} + +// src/helpers/copyToClipboard.ts +function copyToClipboard(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + showToast(_("Successfully copied!"), "success"); + } catch (_err) { + showToast(_("Failed to copy!"), "error"); + console.error("copyToClipboard - e", _err); + } + document.body.removeChild(textarea); +} + +// src/partials/modal/renderModal.ts +function renderModal(text, name) { + return E( + "div", + { class: "pdk-partial-modal__body" }, + E("div", {}, [ + E("pre", { class: "pdk-partial-modal__content" }, E("code", {}, text)), + E("div", { class: "pdk-partial-modal__footer" }, [ + renderButton({ + classNames: ["cbi-button-apply"], + text: _("Download"), + onClick: () => downloadAsTxt(text, name) + }), + renderButton({ + classNames: ["cbi-button-apply"], + text: _("Copy"), + onClick: () => copyToClipboard(` \`\`\`${name} + ${text} + \`\`\``) + }), + renderButton({ + classNames: ["cbi-button-remove"], + text: _("Close"), + onClick: ui.hideModal + }) + ]) + ]) + ); +} + +// src/partials/index.ts +var PartialStyles = ` +${styles2} +${styles3} +`; + +// src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +function renderAvailableActions({ + restart, + start, + stop, + enable, + disable, + globalCheck, + viewLogs, + showSingBoxConfig +}) { + return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ + E("b", {}, "Available actions"), + ...insertIf(restart.visible, [ + renderButton({ + classNames: ["cbi-button-apply"], + onClick: restart.onClick, + icon: renderRotateCcwIcon24, + text: _("Restart podkop"), + loading: restart.loading, + disabled: restart.disabled + }) + ]), + ...insertIf(stop.visible, [ + renderButton({ + classNames: ["cbi-button-remove"], + onClick: stop.onClick, + icon: renderCircleStopIcon24, + text: _("Stop podkop"), + loading: stop.loading, + disabled: stop.disabled + }) + ]), + ...insertIf(start.visible, [ + renderButton({ + classNames: ["cbi-button-save"], + onClick: start.onClick, + icon: renderCirclePlayIcon24, + text: _("Start podkop"), + loading: start.loading, + disabled: start.disabled + }) + ]), + ...insertIf(disable.visible, [ + renderButton({ + classNames: ["cbi-button-remove"], + onClick: disable.onClick, + icon: renderPauseIcon24, + text: _("Disable autostart"), + loading: disable.loading, + disabled: disable.disabled + }) + ]), + ...insertIf(enable.visible, [ + renderButton({ + classNames: ["cbi-button-save"], + onClick: enable.onClick, + icon: renderPlayIcon24, + text: _("Enable autostart"), + loading: enable.loading, + disabled: enable.disabled + }) + ]), + ...insertIf(globalCheck.visible, [ + renderButton({ + onClick: globalCheck.onClick, + icon: renderCircleCheckBigIcon24, + text: _("Get global check"), + loading: globalCheck.loading, + disabled: globalCheck.disabled + }) + ]), + ...insertIf(viewLogs.visible, [ + renderButton({ + onClick: viewLogs.onClick, + icon: renderSquareChartGanttIcon24, + text: _("View logs"), + loading: viewLogs.loading, + disabled: viewLogs.disabled + }) + ]), + ...insertIf(showSingBoxConfig.visible, [ + renderButton({ + onClick: showSingBoxConfig.onClick, + icon: renderCogIcon24, + text: _("Show sing-box config"), + loading: showSingBoxConfig.loading, + disabled: showSingBoxConfig.disabled + }) + ]) + ]); +} + +// src/podkop/tabs/diagnostic/partials/renderCheckSection.ts +function renderCheckSummary(items) { + if (!items.length) { + return E("div", {}, ""); + } + const renderedItems = items.map((item) => { + function getIcon() { + const iconWrap = E("span", { + class: "pdk_diagnostic_alert__summary__item__icon" + }); + if (item.state === "success") { + iconWrap.appendChild(renderCheckIcon24()); + } + if (item.state === "warning") { + iconWrap.appendChild(renderTriangleAlertIcon24()); + } + if (item.state === "error") { + iconWrap.appendChild(renderXIcon24()); + } + return iconWrap; + } + return E( + "div", + { + class: `pdk_diagnostic_alert__summary__item pdk_diagnostic_alert__summary__item--${item.state}` + }, + [getIcon(), E("b", {}, item.key), E("div", {}, item.value)] + ); + }); + return E("div", { class: "pdk_diagnostic_alert__summary" }, renderedItems); +} +function renderLoadingState3(props) { + const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); + iconWrap.appendChild(renderLoaderCircleIcon24()); + return E( + "div", + { class: "pdk_diagnostic_alert pdk_diagnostic_alert--loading" }, + [ + iconWrap, + E("div", { class: "pdk_diagnostic_alert__content" }, [ + E("b", { class: "pdk_diagnostic_alert__title" }, props.title), + E( + "div", + { class: "pdk_diagnostic_alert__description" }, + props.description + ) + ]), + E("div", {}, ""), + renderCheckSummary(props.items) + ] + ); +} +function renderWarningState(props) { + const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); + iconWrap.appendChild(renderCircleAlertIcon24()); + return E( + "div", + { class: "pdk_diagnostic_alert pdk_diagnostic_alert--warning" }, + [ + iconWrap, + E("div", { class: "pdk_diagnostic_alert__content" }, [ + E("b", { class: "pdk_diagnostic_alert__title" }, props.title), + E( + "div", + { class: "pdk_diagnostic_alert__description" }, + props.description + ) + ]), + E("div", {}, ""), + renderCheckSummary(props.items) + ] + ); +} +function renderErrorState(props) { + const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); + iconWrap.appendChild(renderCircleXIcon24()); + return E( + "div", + { class: "pdk_diagnostic_alert pdk_diagnostic_alert--error" }, + [ + iconWrap, + E("div", { class: "pdk_diagnostic_alert__content" }, [ + E("b", { class: "pdk_diagnostic_alert__title" }, props.title), + E( + "div", + { class: "pdk_diagnostic_alert__description" }, + props.description + ) + ]), + E("div", {}, ""), + renderCheckSummary(props.items) + ] + ); +} +function renderSuccessState(props) { + const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); + iconWrap.appendChild(renderCircleCheckIcon24()); + return E( + "div", + { class: "pdk_diagnostic_alert pdk_diagnostic_alert--success" }, + [ + iconWrap, + E("div", { class: "pdk_diagnostic_alert__content" }, [ + E("b", { class: "pdk_diagnostic_alert__title" }, props.title), + E( + "div", + { class: "pdk_diagnostic_alert__description" }, + props.description + ) + ]), + E("div", {}, ""), + renderCheckSummary(props.items) + ] + ); +} +function renderSkippedState(props) { + const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); + iconWrap.appendChild(renderCircleSlashIcon24()); + return E( + "div", + { class: "pdk_diagnostic_alert pdk_diagnostic_alert--skipped" }, + [ + iconWrap, + E("div", { class: "pdk_diagnostic_alert__content" }, [ + E("b", { class: "pdk_diagnostic_alert__title" }, props.title), + E( + "div", + { class: "pdk_diagnostic_alert__description" }, + props.description + ) + ]), + E("div", {}, ""), + renderCheckSummary(props.items) + ] + ); +} +function renderCheckSection(props) { + if (props.state === "loading") { + return renderLoadingState3(props); + } + if (props.state === "warning") { + return renderWarningState(props); + } + if (props.state === "error") { + return renderErrorState(props); + } + if (props.state === "success") { + return renderSuccessState(props); + } + if (props.state === "skipped") { + return renderSkippedState(props); + } + return E("div", {}, _("Not implement yet")); +} + +// src/podkop/tabs/diagnostic/partials/renderRunAction.ts +function renderRunAction({ + loading, + click +}) { + return E("div", { class: "pdk_diagnostic-page__run_check_wrapper" }, [ + renderButton({ + text: _("Run Diagnostic"), + onClick: click, + icon: renderSearchIcon24, + loading, + classNames: ["cbi-button-apply"] + }) + ]); +} + +// src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts +function renderSystemInfo({ items }) { + return E("div", { class: "pdk_diagnostic-page__right-bar__system-info" }, [ + E( + "b", + { class: "pdk_diagnostic-page__right-bar__system-info__title" }, + "System information" + ), + ...items.map((item) => { + const tagClass = [ + "pdk_diagnostic-page__right-bar__system-info__row__tag", + ...insertIf(item.tag?.kind === "warning", [ + "pdk_diagnostic-page__right-bar__system-info__row__tag--warning" + ]), + ...insertIf(item.tag?.kind === "success", [ + "pdk_diagnostic-page__right-bar__system-info__row__tag--success" + ]) + ].filter(Boolean).join(" "); + return E( + "div", + { class: "pdk_diagnostic-page__right-bar__system-info__row" }, + [ + E("b", {}, item.key), + E("div", {}, [ + E("span", {}, item.value), + E("span", { class: tagClass }, item?.tag?.label) + ]) + ] + ); + }) + ]); +} + +// src/helpers/normalizeCompiledVersion.ts +function normalizeCompiledVersion(version) { + if (version.includes("COMPILED")) { + return "dev"; + } + return version; +} + +// src/podkop/tabs/diagnostic/initController.ts +async function fetchSystemInfo() { + const systemInfo = await PodkopShellMethods.getSystemInfo(); + if (systemInfo.success) { + store.set({ + diagnosticsSystemInfo: { + loading: false, + ...systemInfo.data + } + }); + } else { + store.set({ + diagnosticsSystemInfo: { + loading: false, + podkop_version: _("unknown"), + podkop_latest_version: _("unknown"), + luci_app_version: _("unknown"), + sing_box_version: _("unknown"), + openwrt_version: _("unknown"), + device_model: _("unknown") + } + }); + } +} +function renderDiagnosticsChecks() { + logger.debug("[DIAGNOSTIC]", "renderDiagnosticsChecks"); + const diagnosticsChecks = store.get().diagnosticsChecks.sort((a, b) => a.order - b.order); + const container = document.getElementById("pdk_diagnostic-page-checks"); + const renderedDiagnosticsChecks = diagnosticsChecks.map( + (check) => renderCheckSection(check) + ); + return preserveScrollForPage(() => { + container.replaceChildren(...renderedDiagnosticsChecks); + }); +} +function renderDiagnosticRunActionWidget() { + logger.debug("[DIAGNOSTIC]", "renderDiagnosticRunActionWidget"); + const { loading } = store.get().diagnosticsRunAction; + const container = document.getElementById("pdk_diagnostic-page-run-check"); + const renderedAction = renderRunAction({ + loading, + click: () => runChecks() + }); + return preserveScrollForPage(() => { + container.replaceChildren(renderedAction); + }); +} +async function handleRestart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: true } + } + }); + try { + await PodkopShellMethods.restart(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleRestart - e", e); + } finally { + setTimeout(async () => { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + restart: { loading: false } + } + }); + store.reset(["diagnosticsChecks"]); + }, 5e3); + } +} +async function handleStop() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: true } + } + }); + try { + await PodkopShellMethods.stop(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleStop - e", e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + stop: { loading: false } + } + }); + store.reset(["diagnosticsChecks"]); + } +} +async function handleStart() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: true } + } + }); + try { + await PodkopShellMethods.start(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleStart - e", e); + } finally { + setTimeout(async () => { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + start: { loading: false } + } + }); + store.reset(["diagnosticsChecks"]); + }, 5e3); + } +} +async function handleEnable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: true } + } + }); + try { + await PodkopShellMethods.enable(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleEnable - e", e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + enable: { loading: false } + } + }); + } +} +async function handleDisable() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: true } + } + }); + try { + await PodkopShellMethods.disable(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleDisable - e", e); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + disable: { loading: false } + } + }); + } +} +async function handleShowGlobalCheck() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + globalCheck: { loading: true } + } + }); + try { + const globalCheck = await PodkopShellMethods.globalCheck(); + if (globalCheck.success) { + ui.showModal( + _("Global check"), + renderModal(globalCheck.data, "global_check") + ); + } + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + globalCheck: { loading: false } + } + }); + } +} +async function handleViewLogs() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + viewLogs: { loading: true } + } + }); + try { + const viewLogs = await PodkopShellMethods.checkLogs(); + if (viewLogs.success) { + ui.showModal( + _("View logs"), + renderModal(viewLogs.data, "view_logs") + ); + } + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleViewLogs - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + viewLogs: { loading: false } + } + }); + } +} +async function handleShowSingBoxConfig() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + showSingBoxConfig: { loading: true } + } + }); + try { + const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); + if (showSingBoxConfig.success) { + ui.showModal( + _("Show sing-box config"), + renderModal(showSingBoxConfig.data, "show_sing_box_config") + ); + } + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleShowSingBoxConfig - e", e); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + showSingBoxConfig: { loading: false } + } + }); + } +} +function renderDiagnosticAvailableActionsWidget() { + const diagnosticsActions = store.get().diagnosticsActions; + const servicesInfoWidget = store.get().servicesInfoWidget; + logger.debug("[DIAGNOSTIC]", "renderDiagnosticAvailableActionsWidget"); + const podkopEnabled = Boolean(servicesInfoWidget.data.podkop); + const singBoxRunning = Boolean(servicesInfoWidget.data.singbox); + const atLeastOneServiceCommandLoading = servicesInfoWidget.loading || diagnosticsActions.restart.loading || diagnosticsActions.start.loading || diagnosticsActions.stop.loading; + const container = document.getElementById("pdk_diagnostic-page-actions"); + const renderedActions = renderAvailableActions({ + restart: { + loading: diagnosticsActions.restart.loading, + visible: true, + onClick: handleRestart, + disabled: atLeastOneServiceCommandLoading + }, + start: { + loading: diagnosticsActions.start.loading, + visible: !singBoxRunning, + onClick: handleStart, + disabled: atLeastOneServiceCommandLoading + }, + stop: { + loading: diagnosticsActions.stop.loading, + visible: singBoxRunning, + onClick: handleStop, + disabled: atLeastOneServiceCommandLoading + }, + enable: { + loading: diagnosticsActions.enable.loading, + visible: !podkopEnabled, + onClick: handleEnable, + disabled: atLeastOneServiceCommandLoading + }, + disable: { + loading: diagnosticsActions.disable.loading, + visible: podkopEnabled, + onClick: handleDisable, + disabled: atLeastOneServiceCommandLoading + }, + globalCheck: { + loading: diagnosticsActions.globalCheck.loading, + visible: true, + onClick: handleShowGlobalCheck, + disabled: atLeastOneServiceCommandLoading + }, + viewLogs: { + loading: diagnosticsActions.viewLogs.loading, + visible: true, + onClick: handleViewLogs, + disabled: atLeastOneServiceCommandLoading + }, + showSingBoxConfig: { + loading: diagnosticsActions.showSingBoxConfig.loading, + visible: true, + onClick: handleShowSingBoxConfig, + disabled: atLeastOneServiceCommandLoading + } + }); + return preserveScrollForPage(() => { + container.replaceChildren(renderedActions); + }); +} +function renderDiagnosticSystemInfoWidget() { + logger.debug("[DIAGNOSTIC]", "renderDiagnosticSystemInfoWidget"); + const diagnosticsSystemInfo = store.get().diagnosticsSystemInfo; + const container = document.getElementById("pdk_diagnostic-page-system-info"); + function getPodkopVersionRow() { + const loading = diagnosticsSystemInfo.loading; + const unknown = diagnosticsSystemInfo.podkop_version === _("unknown"); + const hasActualVersion = Boolean( + diagnosticsSystemInfo.podkop_latest_version + ); + const version = normalizeCompiledVersion( + diagnosticsSystemInfo.podkop_version + ); + const isDevVersion = version === "dev"; + if (loading || unknown || !hasActualVersion || isDevVersion) { + return { + key: "Podkop", + value: version + }; + } + if (version !== diagnosticsSystemInfo.podkop_latest_version) { + return { + key: "Podkop", + value: version, + tag: { + label: _("Outdated"), + kind: "warning" + } + }; + } + return { + key: "Podkop", + value: version, + tag: { + label: _("Latest"), + kind: "success" + } + }; + } + const renderedSystemInfo = renderSystemInfo({ + items: [ + getPodkopVersionRow(), + { + key: "Luci App", + value: normalizeCompiledVersion(diagnosticsSystemInfo.luci_app_version) + }, + { + key: "Sing-box", + value: diagnosticsSystemInfo.sing_box_version + }, + { + key: "OS", + value: diagnosticsSystemInfo.openwrt_version + }, + { + key: "Device", + value: diagnosticsSystemInfo.device_model + } + ] + }); + return preserveScrollForPage(() => { + container.replaceChildren(renderedSystemInfo); + }); +} +async function onStoreUpdate2(next, prev, diff) { + if (diff.diagnosticsChecks) { + renderDiagnosticsChecks(); + } + if (diff.diagnosticsRunAction) { + renderDiagnosticRunActionWidget(); + } + if (diff.diagnosticsActions || diff.servicesInfoWidget) { + renderDiagnosticAvailableActionsWidget(); + } + if (diff.diagnosticsSystemInfo) { + renderDiagnosticSystemInfoWidget(); + } +} +async function runChecks() { + try { + store.set({ + diagnosticsRunAction: { loading: true }, + diagnosticsChecks: loadingDiagnosticsChecksStore.diagnosticsChecks + }); + await runDnsCheck(); + await runSingBoxCheck(); + await runNftCheck(); + await runFakeIPCheck(); + } catch (e) { + logger.error("[DIAGNOSTIC]", "runChecks - e", e); + } finally { + store.set({ diagnosticsRunAction: { loading: false } }); + } +} +function onPageMount2() { + onPageUnmount2(); + store.subscribe(onStoreUpdate2); + renderDiagnosticsChecks(); + renderDiagnosticRunActionWidget(); + renderDiagnosticAvailableActionsWidget(); + renderDiagnosticSystemInfoWidget(); + fetchServicesInfo(); + fetchSystemInfo(); +} +function onPageUnmount2() { + store.unsubscribe(onStoreUpdate2); + store.reset([ + "diagnosticsActions", + "diagnosticsSystemInfo", + "diagnosticsChecks", + "diagnosticsRunAction" + ]); +} +function registerLifecycleListeners2() { + store.subscribe((next, prev, diff) => { + if (diff.tabService && next.tabService.current !== prev.tabService.current) { + logger.debug( + "[DIAGNOSTIC]", + "active tab diff event, active tab:", + diff.tabService.current + ); + const isDIAGNOSTICVisible = next.tabService.current === "diagnostic"; + if (isDIAGNOSTICVisible) { + logger.debug( + "[DIAGNOSTIC]", + "registerLifecycleListeners", + "onPageMount" + ); + return onPageMount2(); + } + if (!isDIAGNOSTICVisible) { + logger.debug( + "[DIAGNOSTIC]", + "registerLifecycleListeners", + "onPageUnmount" + ); + return onPageUnmount2(); + } + } + }); +} +async function initController2() { + onMount("diagnostic-status").then(() => { + logger.debug("[DIAGNOSTIC]", "initController", "onMount"); + onPageMount2(); + registerLifecycleListeners2(); + }); +} + +// src/podkop/tabs/diagnostic/styles.ts +var styles4 = ` + +#cbi-podkop-diagnostic-_mount_node > div { + width: 100%; +} + +#cbi-podkop-diagnostic > h3 { + display: none; +} + +.pdk_diagnostic-page { + display: grid; + grid-template-columns: 2fr 1fr; + grid-column-gap: 10px; + align-items: start; +} + +@media (max-width: 800px) { + .pdk_diagnostic-page { + grid-template-columns: 1fr; + } +} + +.pdk_diagnostic-page__right-bar { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__right-bar__actions { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + + display: grid; + grid-template-columns: auto; + grid-row-gap: 10px; + +} + +.pdk_diagnostic-page__right-bar__system-info { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + + display: grid; + grid-template-columns: auto; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__right-bar__system-info__title { + +} + +.pdk_diagnostic-page__right-bar__system-info__row { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 5px; +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag { + padding: 2px 4px; + border: 1px transparent solid; + border-radius: 4px; + margin-left: 5px; +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag--warning { + border: 1px var(--warn-color-medium, orange) solid; + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic-page__right-bar__system-info__row__tag--success { + border: 1px var(--success-color-medium, green) solid; + color: var(--success-color-medium, green); +} + +.pdk_diagnostic-page__left-bar { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic-page__run_check_wrapper {} + +.pdk_diagnostic-page__run_check_wrapper button { + width: 100%; +} + +.pdk_diagnostic-page__checks { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_diagnostic_alert { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + + display: grid; + grid-template-columns: 24px 1fr; + grid-column-gap: 10px; + align-items: center; + padding: 10px; +} + +.pdk_diagnostic_alert--loading { + border: 2px var(--primary-color-high, dodgerblue) solid; +} + +.pdk_diagnostic_alert--warning { + border: 2px var(--warn-color-medium, orange) solid; + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic_alert--error { + border: 2px var(--error-color-medium, red) solid; + color: var(--error-color-medium, red); +} + +.pdk_diagnostic_alert--success { + border: 2px var(--success-color-medium, green) solid; + color: var(--success-color-medium, green); +} + +.pdk_diagnostic_alert--skipped {} + +.pdk_diagnostic_alert__icon {} + +.pdk_diagnostic_alert__content {} + +.pdk_diagnostic_alert__title { + display: block; +} + +.pdk_diagnostic_alert__description {} + +.pdk_diagnostic_alert__summary { + margin-top: 10px; +} + +.pdk_diagnostic_alert__summary__item { + display: grid; + grid-template-columns: 16px auto 1fr; + grid-column-gap: 10px; +} + +.pdk_diagnostic_alert__summary__item--error { + color: var(--error-color-medium, red); +} + +.pdk_diagnostic_alert__summary__item--warning { + color: var(--warn-color-medium, orange); +} + +.pdk_diagnostic_alert__summary__item--success { + color: var(--success-color-medium, green); +} + +.pdk_diagnostic_alert__summary__item__icon { + width: 16px; + height: 16px; +} +`; + +// src/podkop/tabs/diagnostic/index.ts +var DiagnosticTab = { + render: render2, + initController: initController2, + styles: styles4 +}; + +// src/styles.ts +var GlobalStyles = ` +${DashboardTab.styles} +${DiagnosticTab.styles} +${PartialStyles} + + +/* Hide extra H3 for settings tab */ +#cbi-podkop-settings > h3 { + display: none; +} + +/* Hide extra H3 for sections tab */ +#cbi-podkop-section > h3:nth-child(1) { + display: none; +} + +/* Vertical align for remove section action button */ +#cbi-podkop-section > .cbi-section-remove { + margin-bottom: -32px; +} + +/* Centered class helper */ +.centered { + display: flex; + align-items: center; + justify-content: center; +} + +/* Rotate class helper */ +.rotate { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Skeleton styles*/ +.skeleton { + background-color: var(--background-color-low, #e0e0e0); + border-radius: 4px; + position: relative; + overflow: hidden; +} + +.skeleton::after { + content: ''; + position: absolute; + top: 0; + left: -150%; + width: 150%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.4), + transparent + ); + animation: skeleton-shimmer 1.6s infinite; +} + +@keyframes skeleton-shimmer { + 100% { + left: 150%; + } +} +/* Toast */ +.toast-container { + position: fixed; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + z-index: 9999; + font-family: system-ui, sans-serif; +} + +.toast { + opacity: 0; + transform: translateY(10px); + transition: opacity 0.3s ease, transform 0.3s ease; + padding: 10px 16px; + border-radius: 6px; + color: #fff; + font-size: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + min-width: 220px; + max-width: 340px; + text-align: center; +} + +.toast-success { + background-color: #28a745; +} + +.toast-error { + background-color: #dc3545; +} + +.toast.visible { + opacity: 1; + transform: translateY(0); +} +`; + +// src/helpers/injectGlobalStyles.ts +function injectGlobalStyles() { + document.head.insertAdjacentHTML( + "beforeend", + ` + + ` + ); +} + +// src/helpers/withTimeout.ts +async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = _("Operation timed out")) { + let timeoutId; + const start = performance.now(); + const timeoutPromise = new Promise((_2, reject) => { + timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); + }); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + clearTimeout(timeoutId); + const elapsed = performance.now() - start; + logger.info("[SHELL]", `[${operationName}] took ${elapsed.toFixed(2)} ms`); + } +} + +// src/helpers/executeShellCommand.ts +async function executeShellCommand({ + command, + args, + timeout = COMMAND_TIMEOUT +}) { + try { + return withTimeout( + fs.exec(command, args), + timeout, + [command, ...args].join(" ") + ); + } catch (err) { + const error = err; + return { stdout: "", stderr: error?.message, code: 0 }; + } +} + +// src/helpers/maskIP.ts +function maskIP(ip = "") { + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`); +} + +// src/helpers/getProxyUrlName.ts +function getProxyUrlName(url) { + try { + const [_link, hash] = url.split("#"); + if (!hash) { + return ""; + } + return decodeURIComponent(hash); + } catch { + return ""; + } +} + +// src/helpers/onMount.ts +async function onMount(id) { + return new Promise((resolve) => { + const el = document.getElementById(id); + if (el && el.offsetParent !== null) { + return resolve(el); + } + const observer = new MutationObserver(() => { + const target = document.getElementById(id); + if (target) { + const io = new IntersectionObserver((entries) => { + const visible = entries.some((e) => e.isIntersecting); + if (visible) { + observer.disconnect(); + io.disconnect(); + resolve(target); + } + }); + io.observe(target); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true + }); + }); +} + +// src/helpers/getClashApiUrl.ts +function getClashWsUrl() { + const { hostname } = window.location; + return `ws://${hostname}:9090`; +} +function getClashUIUrl() { + const { hostname } = window.location; + return `http://${hostname}:9090/ui`; +} + +// src/helpers/splitProxyString.ts +function splitProxyString(str) { + return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean); +} + +// src/helpers/preserveScrollForPage.ts +function preserveScrollForPage(renderFn) { + const scrollY = window.scrollY; + renderFn(); + requestAnimationFrame(() => { + window.scrollTo({ top: scrollY }); + }); +} + +// src/helpers/svgEl.ts +function svgEl(tag, attrs = {}, children = []) { + const NS = "http://www.w3.org/2000/svg"; + const el = document.createElementNS(NS, tag); + for (const [k, v] of Object.entries(attrs)) { + if (v != null) el.setAttribute(k, String(v)); + } + (Array.isArray(children) ? children : [children]).filter(Boolean).forEach((ch) => el.appendChild(ch)); + return el; +} + +// src/helpers/insertIf.ts +function insertIf(condition, elements) { + return condition ? elements : []; +} +function insertIfObj(condition, object) { + return condition ? object : {}; +} return baseclass.extend({ ALLOWED_WITH_RUSSIA_INSIDE, BOOTSTRAP_DNS_SERVER_OPTIONS, @@ -1958,49 +4515,45 @@ return baseclass.extend({ CACHE_TIMEOUT, COMMAND_SCHEDULING, COMMAND_TIMEOUT, + CustomPodkopMethods, DIAGNOSTICS_INITIAL_DELAY, DIAGNOSTICS_UPDATE_INTERVAL, DNS_SERVER_OPTIONS, DOMAIN_LIST_OPTIONS, + DashboardTab, + DiagnosticTab, ERROR_POLL_INTERVAL, FAKEIP_CHECK_DOMAIN, FETCH_TIMEOUT, IP_CHECK_DOMAIN, + Logger, PODKOP_LUCI_APP_VERSION, + PodkopShellMethods, REGIONAL_OPTIONS, + RemoteFakeIPMethods, STATUS_COLORS, TabService, TabServiceInstance, UPDATE_INTERVAL_OPTIONS, bulkValidate, coreService, - createBaseApiRequest, executeShellCommand, - getBaseUrl, - getClashApiUrl, - getClashConfig, - getClashGroupDelay, - getClashProxies, getClashUIUrl, - getClashVersion, getClashWsUrl, - getConfigSections, - getDashboardSections, - getPodkopStatus, getProxyUrlName, - getSingboxStatus, - initDashboardController, injectGlobalStyles, + insertIf, + insertIfObj, + logger, maskIP, onMount, parseQueryString, parseValueList, preserveScrollForPage, - renderDashboard, + socket, splitProxyString, - triggerLatencyGroupTest, - triggerLatencyProxyTest, - triggerProxySelector, + store, + svgEl, validateDNS, validateDomain, validateIPV4, @@ -2008,6 +4561,7 @@ return baseclass.extend({ validatePath, validateProxyUrl, validateShadowsocksUrl, + validateSocksUrl, validateSubnet, validateTrojanUrl, validateUrl, diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js index c84ff91..f699e17 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -1,82 +1,98 @@ -'use strict'; -'require view'; -'require form'; -'require network'; -'require view.podkop.configSection as configSection'; -'require view.podkop.diagnosticTab as diagnosticTab'; -'require view.podkop.additionalTab as additionalTab'; -'require view.podkop.dashboardTab as dashboardTab'; -'require view.podkop.utils as utils'; -'require view.podkop.main as main'; +"use strict"; +"require view"; +"require form"; +"require baseclass"; +"require network"; +"require view.podkop.main as main"; -const EntryNode = { +// Settings content +"require view.podkop.settings as settings"; + +// Sections content +"require view.podkop.section as section"; + +// Dashboard content +"require view.podkop.dashboard as dashboard"; + +// Diagnostic content +"require view.podkop.diagnostic as diagnostic"; + +const EntryPoint = { async render() { main.injectGlobalStyles(); - const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); - - // Main Section - const mainSection = podkopFormMap.section(form.TypedSection, 'main'); - mainSection.anonymous = true; - - configSection.createConfigSection(mainSection); - - // Additional Settings Tab (main section) - additionalTab.createAdditionalSection(mainSection); - - // Diagnostics Tab (main section) - diagnosticTab.createDiagnosticsSection(mainSection); - const podkopFormMapPromise = podkopFormMap.render().then((node) => { - // Set up diagnostics event handlers - diagnosticTab.setupDiagnosticsEventHandlers(node); - - // Start critical error polling for all tabs - utils.startErrorPolling(); - - // Add event listener to keep error polling active when switching tabs - const tabs = node.querySelectorAll('.cbi-tabmenu'); - if (tabs.length > 0) { - tabs[0].addEventListener('click', function (e) { - const tab = e.target.closest('.cbi-tab'); - if (tab) { - // Ensure error polling continues when switching tabs - utils.startErrorPolling(); - } - }); - } - - // Add visibility change handler to manage error polling - document.addEventListener('visibilitychange', function () { - if (document.hidden) { - utils.stopErrorPolling(); - } else { - utils.startErrorPolling(); - } - }); - - return node; - }); - - // Extra Section - const extraSection = podkopFormMap.section( - form.TypedSection, - 'extra', - _('Extra configurations'), + const podkopMap = new form.Map( + "podkop", + _("Podkop Settings"), + _("Configuration for Podkop service"), ); - extraSection.anonymous = false; - extraSection.addremove = true; - extraSection.addbtntitle = _('Add Section'); - extraSection.multiple = true; - configSection.createConfigSection(extraSection); + // Enable tab views + podkopMap.tabbed = true; - // Initial dashboard render - dashboardTab.createDashboardSection(mainSection); + // Sections tab + const sectionsSection = podkopMap.section( + form.TypedSection, + "section", + _("Sections"), + ); + sectionsSection.anonymous = false; + sectionsSection.addremove = true; + sectionsSection.template = "cbi/simpleform"; + + // Render section content + section.createSectionContent(sectionsSection); + + // Settings tab + const settingsSection = podkopMap.section( + form.TypedSection, + "settings", + _("Settings"), + ); + settingsSection.anonymous = true; + settingsSection.addremove = false; + // Make it named [ config settings 'settings' ] + settingsSection.cfgsections = function () { + return ["settings"]; + }; + + // Render settings content + settings.createSettingsContent(settingsSection); + + // Diagnostic tab + const diagnosticSection = podkopMap.section( + form.TypedSection, + "diagnostic", + _("Diagnostics"), + ); + diagnosticSection.anonymous = true; + diagnosticSection.addremove = false; + diagnosticSection.cfgsections = function () { + return ["diagnostic"]; + }; + + // Render diagnostic content + diagnostic.createDiagnosticContent(diagnosticSection); + + // Dashboard tab + const dashboardSection = podkopMap.section( + form.TypedSection, + "dashboard", + _("Dashboard"), + ); + dashboardSection.anonymous = true; + dashboardSection.addremove = false; + dashboardSection.cfgsections = function () { + return ["dashboard"]; + }; + + // Render dashboard content + dashboard.createDashboardContent(dashboardSection); // Inject core service main.coreService(); - return podkopFormMapPromise; + return podkopMap.render(); }, }; -return view.extend(EntryNode); +return view.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js new file mode 100644 index 0000000..54f5bd3 --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -0,0 +1,600 @@ +"use strict"; +"require form"; +"require baseclass"; +"require ui"; +"require tools.widgets as widgets"; +"require view.podkop.main as main"; + +function createSectionContent(section) { + let o = section.option( + form.ListValue, + "connection_type", + _("Connection Type"), + _("Select between VPN and Proxy connection methods for traffic routing"), + ); + o.value("proxy", "Proxy"); + o.value("vpn", "VPN"); + o.value("block", "Block"); + + o = section.option( + form.ListValue, + "proxy_config_type", + _("Configuration Type"), + _("Select how to configure the proxy"), + ); + o.value("url", _("Connection URL")); + o.value("outbound", _("Outbound Config")); + o.value("urltest", _("URLTest")); + o.default = "url"; + o.depends("connection_type", "proxy"); + + o = section.option( + form.TextValue, + "proxy_string", + _("Proxy Configuration URL"), + "", + ); + o.depends("proxy_config_type", "url"); + o.rows = 5; + // Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links) + o.wrap = "soft"; + // Render as a textarea to allow multiple proxy URLs/configs + o.textarea = true; + o.rmempty = false; + o.sectionDescriptions = new Map(); + o.placeholder = "vless://uuid@server:port?type=tcp&security=tls#main"; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateProxyUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.TextValue, + "outbound_json", + _("Outbound Configuration"), + _("Enter complete outbound configuration in JSON format"), + ); + o.depends("proxy_config_type", "outbound"); + o.rows = 10; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateOutboundJson(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "urltest_proxy_links", + _("URLTest Proxy Links"), + ); + o.depends("proxy_config_type", "urltest"); + o.placeholder = "vless://, ss://, trojan://, socks4/5:// links"; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateProxyUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + "enable_udp_over_tcp", + _("UDP over TCP"), + _("Applicable for SOCKS and Shadowsocks proxy"), + ); + o.default = "0"; + o.depends("connection_type", "proxy"); + o.rmempty = false; + + o = section.option( + widgets.DeviceSelect, + "interface", + _("Network Interface"), + _("Select network interface for VPN connection"), + ); + o.depends("connection_type", "vpn"); + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.filter = function (section_id, value) { + // Blocked interface names that should never be selectable + const blockedInterfaces = [ + "br-lan", + "eth0", + "eth1", + "wan", + "phy0-ap0", + "phy1-ap0", + "pppoe-wan", + "lan", + ]; + + // Reject immediately if the value matches any blocked interface + if (blockedInterfaces.includes(value)) { + return false; + } + + // Try to find the device object with the given name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Get the device type (e.g., "wifi", "ethernet", etc.) + const type = device.getType(); + + // Reject wireless-related devices + const isWireless = + type === "wifi" || type === "wireless" || type.includes("wlan"); + + return !isWireless; + }; + + o = section.option( + form.Flag, + "domain_resolver_enabled", + _("Domain Resolver"), + _("Enable built-in DNS resolver for domains handled by this section"), + ); + o.default = "0"; + o.rmempty = false; + o.depends("connection_type", "vpn"); + + o = section.option( + form.ListValue, + "domain_resolver_dns_type", + _("DNS Protocol Type"), + _("Select the DNS protocol type for the domain resolver"), + ); + o.value("doh", _("DNS over HTTPS (DoH)")); + o.value("dot", _("DNS over TLS (DoT)")); + o.value("udp", _("UDP (Unprotected DNS)")); + o.default = "udp"; + o.rmempty = false; + o.depends("domain_resolver_enabled", "1"); + + o = section.option( + form.Value, + "domain_resolver_dns_server", + _("DNS Server"), + _("Select or enter DNS server address"), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = "8.8.8.8"; + o.rmempty = false; + o.depends("domain_resolver_enabled", "1"); + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "community_lists", + _("Community Lists"), + _("Select a predefined list for routing") + + ' github.com/itdoginfo/allow-domains', + ); + o.placeholder = "Service list"; + Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.rmempty = true; + let lastValues = []; + let isProcessing = false; + + o.onchange = function (ev, section_id, value) { + if (isProcessing) return; + isProcessing = true; + + try { + const values = Array.isArray(value) ? value : [value]; + let newValues = [...values]; + let notifications = []; + + const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => + newValues.includes(opt), + ); + + if (selectedRegionalOptions.length > 1) { + const lastSelected = + selectedRegionalOptions[selectedRegionalOptions.length - 1]; + const removedRegions = selectedRegionalOptions.slice(0, -1); + newValues = newValues.filter( + (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), + ); + notifications.push( + E("p", { class: "alert-message warning" }, [ + E("strong", {}, _("Regional options cannot be used together")), + E("br"), + _( + "Warning: %s cannot be used together with %s. Previous selections have been removed.", + ).format(removedRegions.join(", "), lastSelected), + ]), + ); + } + + if (newValues.includes("russia_inside")) { + const removedServices = newValues.filter( + (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), + ); + if (removedServices.length > 0) { + newValues = newValues.filter((v) => + main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), + ); + notifications.push( + E("p", { class: "alert-message warning" }, [ + E("strong", {}, _("Russia inside restrictions")), + E("br"), + _( + "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", + ).format( + main.ALLOWED_WITH_RUSSIA_INSIDE.map( + (key) => main.DOMAIN_LIST_OPTIONS[key], + ) + .filter((label) => label !== "Russia inside") + .join(", "), + removedServices.join(", "), + ), + ]), + ); + } + } + + if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { + this.getUIElement(section_id).setValue(newValues); + } + + notifications.forEach((notification) => + ui.addNotification(null, notification), + ); + lastValues = newValues; + } catch (e) { + console.error("Error in onchange handler:", e); + } finally { + isProcessing = false; + } + }; + + o = section.option( + form.ListValue, + "user_domain_list_type", + _("User Domain List Type"), + _("Select the list type for adding custom domains"), + ); + o.value("disabled", _("Disabled")); + o.value("dynamic", _("Dynamic List")); + o.value("text", _("Text List")); + o.default = "disabled"; + o.rmempty = false; + + o = section.option( + form.DynamicList, + "user_domains", + _("User Domains"), + _( + "Enter domain names without protocols, e.g. example.com or sub.example.com", + ), + ); + o.placeholder = "Domains list"; + o.depends("user_domain_list_type", "dynamic"); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateDomain(value, true); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.TextValue, + "user_domains_text", + _("User Domains List"), + _( + "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", + ), + ); + o.placeholder = + "example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains"; + o.depends("user_domain_list_type", "text"); + o.rows = 8; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const domains = main.parseValueList(value); + + if (!domains.length) { + return _( + "At least one valid domain must be specified. Comments-only content is not allowed.", + ); + } + + const { valid, results } = main.bulkValidate(domains, (row) => + main.validateDomain(row, true), + ); + + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => `${validation.value}: ${validation.message}`); // Collect validation errors + + return [_("Validation errors:"), ...errors].join("\n"); + } + + return true; + }; + + o = section.option( + form.ListValue, + "user_subnet_list_type", + _("User Subnet List Type"), + _("Select the list type for adding custom subnets"), + ); + o.value("disabled", _("Disabled")); + o.value("dynamic", _("Dynamic List")); + o.value("text", _("Text List (comma/space/newline separated)")); + o.default = "disabled"; + o.rmempty = false; + + o = section.option( + form.DynamicList, + "user_subnets", + _("User Subnets"), + _( + "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", + ), + ); + o.placeholder = "IP or subnet"; + o.depends("user_subnet_list_type", "dynamic"); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateSubnet(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.TextValue, + "user_subnets_text", + _("User Subnets List"), + _( + "Enter subnets in CIDR notation or single IP addresses, separated by commas, spaces, or newlines. " + + "You can add comments using //", + ), + ); + o.placeholder = + "103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9"; + o.depends("user_subnet_list_type", "text"); + o.rows = 10; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const subnets = main.parseValueList(value); + + if (!subnets.length) { + return _( + "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", + ); + } + + const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); + + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => `${validation.value}: ${validation.message}`); // Collect validation errors + + return [_("Validation errors:"), ...errors].join("\n"); + } + + return true; + }; + + o = section.option( + form.DynamicList, + "local_domain_lists", + _("Local Domain Lists"), + _("Specify the path to the list file located on the router filesystem"), + ); + o.placeholder = "/path/file.lst"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "local_subnet_lists", + _("Local Subnet Lists"), + _("Specify the path to the list file located on the router filesystem"), + ); + o.placeholder = "/path/file.lst"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "remote_domain_lists", + _("Remote Domain Lists"), + _("Specify remote URLs to download and use domain lists"), + ); + o.placeholder = "https://example.com/domains.srs"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "remote_subnet_lists", + _("Remote Subnet Lists"), + _("Specify remote URLs to download and use subnet lists"), + ); + o.placeholder = "https://example.com/subnets.srs"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + "fully_routed_ips", + _("Fully Routed IPs"), + _( + "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", + ), + ); + o.placeholder = "192.168.1.2 or 192.168.1.0/24"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateSubnet(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + "mixed_proxy_enabled", + _("Enable Mixed Proxy"), + _( + "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.Value, + "mixed_proxy_port", + _("Mixed Proxy Port"), + _( + "Specify the port number on which the mixed proxy will run for this section. " + + "Make sure the selected port is not used by another service", + ), + ); + o.rmempty = false; + o.depends("mixed_proxy_enabled", "1"); +} + +const EntryPoint = { + createSectionContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js new file mode 100644 index 0000000..ad678a4 --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js @@ -0,0 +1,401 @@ +"use strict"; +"require form"; +"require uci"; +"require baseclass"; +"require tools.widgets as widgets"; +"require view.podkop.main as main"; + +function createSettingsContent(section) { + let o = section.option( + form.ListValue, + "dns_type", + _("DNS Protocol Type"), + _("Select DNS protocol to use"), + ); + o.value("doh", _("DNS over HTTPS (DoH)")); + o.value("dot", _("DNS over TLS (DoT)")); + o.value("udp", _("UDP (Unprotected DNS)")); + o.default = "udp"; + o.rmempty = false; + + o = section.option( + form.Value, + "dns_server", + _("DNS Server"), + _("Select or enter DNS server address"), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = "8.8.8.8"; + o.rmempty = false; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Value, + "bootstrap_dns_server", + _("Bootstrap DNS server"), + _( + "The DNS server used to look up the IP address of an upstream DNS server", + ), + ); + Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = "77.88.8.8"; + o.rmempty = false; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Value, + "dns_rewrite_ttl", + _("DNS Rewrite TTL"), + _("Time in seconds for DNS record caching (default: 60)"), + ); + o.default = "60"; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _("TTL value cannot be empty"); + } + + const ttl = parseInt(value); + if (isNaN(ttl) || ttl < 0) { + return _("TTL must be a positive number"); + } + + return true; + }; + + o = section.option( + widgets.DeviceSelect, + "source_network_interfaces", + _("Source Network Interface"), + _("Select the network interface from which the traffic will originate"), + ); + o.default = "br-lan"; + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.multiple = true; + o.filter = function (section_id, value) { + // Block specific interface names from being selectable + const blocked = ["wan", "phy0-ap0", "phy1-ap0", "pppoe-wan"]; + if (blocked.includes(value)) { + return false; + } + + // Try to find the device object by its name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Check the type of the device + const type = device.getType(); + + // Consider any Wi-Fi / wireless / wlan device as invalid + const isWireless = + type === "wifi" || type === "wireless" || type.includes("wlan"); + + // Allow only non-wireless devices + return !isWireless; + }; + + o = section.option( + form.Flag, + "enable_output_network_interface", + _("Enable Output Network Interface"), + _("You can select Output Network Interface, by default autodetect"), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + widgets.DeviceSelect, + "output_network_interface", + _("Output Network Interface"), + _("Select the network interface to which the traffic will originate"), + ); + o.noaliases = true; + o.multiple = false; + o.depends("enable_output_network_interface", "1"); + o.filter = function (section_id, value) { + // Blocked interface names that should never be selectable + const blockedInterfaces = ["br-lan"]; + + // Reject immediately if the value matches any blocked interface + if (blockedInterfaces.includes(value)) { + return false; + } + + // Reject lan* + if ( + value.startsWith("lan") + ) { + return false; + } + + // Reject tun*, wg*, vpn*, awg*, oc* + if ( + value.startsWith("tun") || + value.startsWith("wg") || + value.startsWith("vpn") || + value.startsWith("awg") || + value.startsWith("oc") + ) { + return false; + } + + // Try to find the device object with the given name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Get the device type (e.g., "wifi", "ethernet", etc.) + const type = device.getType(); + + // Reject wireless-related devices + const isWireless = + type === "wifi" || type === "wireless" || type.includes("wlan"); + + return !isWireless; + }; + + o = section.option( + form.Flag, + "enable_badwan_interface_monitoring", + _("Interface Monitoring"), + _("Interface monitoring for Bad WAN"), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + widgets.NetworkSelect, + "badwan_monitored_interfaces", + _("Monitored Interfaces"), + _("Select the WAN interfaces to be monitored"), + ); + o.depends("enable_badwan_interface_monitoring", "1"); + o.multiple = true; + o.filter = function (section_id, value) { + // Reject if the value is in the blocked list ['lan', 'loopback'] + if (["lan", "loopback"].includes(value)) { + return false; + } + + // Reject if the value starts with '@' (means it's an alias/reference) + if (value.startsWith("@")) { + return false; + } + + // Otherwise allow it + return true; + }; + + o = section.option( + form.Value, + "badwan_reload_delay", + _("Interface Monitoring Delay"), + _("Delay in milliseconds before reloading podkop after interface UP"), + ); + o.depends("enable_badwan_interface_monitoring", "1"); + o.default = "2000"; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _("Delay value cannot be empty"); + } + return true; + }; + + o = section.option( + form.Flag, + "enable_yacd", + _("Enable YACD"), + `${main.getClashUIUrl()}`, + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.Flag, + "disable_quic", + _("Disable QUIC"), + _( + "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.ListValue, + "update_interval", + _("List Update Frequency"), + _("Select how often the domain or subnet lists are updated automatically"), + ); + Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = "1d"; + o.rmempty = false; + + o = section.option( + form.Flag, + "download_lists_via_proxy", + _("Download Lists via Proxy/VPN"), + _("Downloading all lists via main Proxy/VPN"), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.ListValue, + "download_lists_via_proxy_section", + _("Download Lists via specific proxy section"), + _("Downloading all lists via specific Proxy/VPN"), + ); + + o.rmempty = false; + o.depends("download_lists_via_proxy", "1"); + o.cfgvalue = function (section_id) { + return uci.get("podkop", section_id, "download_lists_via_proxy_section"); + }; + o.load = function () { + const sections = this.map?.data?.state?.values?.podkop ?? {}; + + this.keylist = []; + this.vallist = []; + + for (const secName in sections) { + const sec = sections[secName]; + if (sec[".type"] === "section") { + this.keylist.push(secName); + this.vallist.push(secName); + } + } + + return Promise.resolve(); + }; + + o = section.option( + form.Flag, + "dont_touch_dhcp", + _("Dont Touch My DHCP!"), + _("Podkop will not modify your DHCP configuration"), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.ListValue, + "config_path", + _("Config File Path"), + _( + "Select path for sing-box config file. Change this ONLY if you know what you are doing", + ), + ); + o.value("/etc/sing-box/config.json", "Flash (/etc/sing-box/config.json)"); + o.value("/tmp/sing-box/config.json", "RAM (/tmp/sing-box/config.json)"); + o.default = "/etc/sing-box/config.json"; + o.rmempty = false; + + o = section.option( + form.Value, + "cache_path", + _("Cache File Path"), + _( + "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", + ), + ); + o.value("/tmp/sing-box/cache.db", "RAM (/tmp/sing-box/cache.db)"); + o.value( + "/usr/share/sing-box/cache.db", + "Flash (/usr/share/sing-box/cache.db)", + ); + o.default = "/tmp/sing-box/cache.db"; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _("Cache file path cannot be empty"); + } + + if (!value.startsWith("/")) { + return _("Path must be absolute (start with /)"); + } + + if (!value.endsWith("cache.db")) { + return _("Path must end with cache.db"); + } + + const parts = value.split("/").filter(Boolean); + if (parts.length < 2) { + return _("Path must contain at least one directory (like /tmp/cache.db)"); + } + + return true; + }; + + o = section.option( + form.Flag, + "exclude_ntp", + _("Exclude NTP"), + _( + "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.DynamicList, + "routing_excluded_ips", + _("Routing Excluded IPs"), + _("Specify a local IP address to be excluded from routing"), + ); + o.placeholder = "IP"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateIPV4(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; +} + +const EntryPoint = { + createSettingsContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js deleted file mode 100644 index f358670..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; -'require baseclass'; -'require ui'; -'require fs'; -'require view.podkop.main as main'; - -// Flag to track if this is the first error check -let isInitialCheck = true; - -// Set to track which errors we've already seen -const lastErrorsSet = new Set(); - -// Timer for periodic error polling -let errorPollTimer = null; - -// Helper function to fetch errors from the podkop command -async function getPodkopErrors() { - return new Promise((resolve) => { - safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => { - if (!result || !result.stdout) return resolve([]); - - const logs = result.stdout.split('\n'); - const errors = logs.filter((log) => log.includes('[critical]')); - - resolve(errors); - }); - }); -} - -// Show error notification to the user -function showErrorNotification(error, isMultiple = false) { - const notificationContent = E('div', { class: 'alert-message error' }, [ - E('pre', { class: 'error-log' }, error), - ]); - - ui.addNotification(null, notificationContent); -} - -// Helper function for command execution with prioritization -function safeExec( - command, - args, - priority, - callback, - timeout = main.COMMAND_TIMEOUT, -) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - const executeCommand = async () => { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - const result = await Promise.race([ - fs.exec(command, args), - new Promise((_, reject) => { - controller.signal.addEventListener('abort', () => { - reject(new Error('Command execution timed out')); - }); - }), - ]); - - clearTimeout(timeoutId); - - if (callback && typeof callback === 'function') { - callback(result); - } - - return result; - } catch (error) { - console.warn( - `Command execution failed or timed out: ${command} ${args.join(' ')}`, - ); - const errorResult = { stdout: '', stderr: error.message, error: error }; - - if (callback && typeof callback === 'function') { - callback(errorResult); - } - - return errorResult; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCommand, schedulingDelay); - return; - } else { - return executeCommand(); - } -} - -// Check for critical errors and show notifications -async function checkForCriticalErrors() { - try { - const errors = await getPodkopErrors(); - - if (errors && errors.length > 0) { - // Filter out errors we've already seen - const newErrors = errors.filter((error) => !lastErrorsSet.has(error)); - - if (newErrors.length > 0) { - // On initial check, just store errors without showing notifications - if (!isInitialCheck) { - // Show each new error as a notification - newErrors.forEach((error) => { - showErrorNotification(error, newErrors.length > 1); - }); - } - - // Add new errors to our set of seen errors - newErrors.forEach((error) => lastErrorsSet.add(error)); - } - } - - // After first check, mark as no longer initial - isInitialCheck = false; - } catch (error) { - console.error('Error checking for critical messages:', error); - } -} - -// Start polling for errors at regular intervals -function startErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - } - - // Reset initial check flag to make sure we show errors - isInitialCheck = false; - - // Immediately check for errors on start - checkForCriticalErrors(); - - // Then set up periodic checks - errorPollTimer = setInterval( - checkForCriticalErrors, - main.ERROR_POLL_INTERVAL, - ); -} - -// Stop polling for errors -function stopErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - errorPollTimer = null; - } -} - -return baseclass.extend({ - startErrorPolling, - stopErrorPolling, - checkForCriticalErrors, - safeExec, -}); diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index 7cfb6bc..1754091 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -1,15 +1,15 @@ -# Russian translations for PODKOP package. +# RU translations for PODKOP package. # Copyright (C) 2025 THE PODKOP'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# Automatically generated, 2025. +# divocat, 2025. # msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-07 16:55+0300\n" -"PO-Revision-Date: 2025-10-07 23:45+0300\n" -"Last-Translator: Automatically generated\n" +"POT-Creation-Date: 2025-10-21 23:02+0300\n" +"PO-Revision-Date: 2025-10-21 23:02+0300\n" +"Last-Translator: divocat\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -17,656 +17,107 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -msgid "Basic Settings" -msgstr "Основные настройки" - -msgid "Connection Type" -msgstr "Тип подключения" - -msgid "Select between VPN and Proxy connection methods for traffic routing" -msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика" - -msgid "Configuration Type" -msgstr "Тип конфигурации" - -msgid "Select how to configure the proxy" -msgstr "Выберите способ настройки прокси" - -msgid "Connection URL" -msgstr "URL подключения" - -msgid "Outbound Config" -msgstr "Конфигурация Outbound" - -msgid "URLTest" -msgstr "URLTest" - -msgid "Proxy Configuration URL" -msgstr "URL конфигурации прокси" - -msgid "Current config: " -msgstr "Текущая конфигурация: " - -msgid "Config without description" -msgstr "Конфигурация без описания" - -msgid "" -"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " -"configs" -msgstr "" -"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для " -"резервных конфигураций" - -msgid "No active configuration found. One configuration is required." -msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка." - -msgid "Multiply active configurations found. Please leave one configuration." -msgstr "Найдено несколько активных конфигураций. Оставьте только одну." - -msgid "Invalid URL format:" -msgstr "Неверный формат URL:" - -msgid "Outbound Configuration" -msgstr "Конфигурация исходящего соединения" - -msgid "Enter complete outbound configuration in JSON format" -msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" - -msgid "URLTest Proxy Links" -msgstr "Ссылки прокси для URLTest" - -msgid "Shadowsocks UDP over TCP" -msgstr "Shadowsocks UDP через TCP" - -msgid "Apply for SS2022" -msgstr "Применить для SS2022" - -msgid "Network Interface" -msgstr "Сетевой интерфейс" - -msgid "Select network interface for VPN connection" -msgstr "Выберите сетевой интерфейс для VPN подключения" - -msgid "Domain Resolver" -msgstr "Резолвер доменов" - -msgid "Enable built-in DNS resolver for domains handled by this section" -msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе" - -msgid "DNS Protocol Type" -msgstr "Тип протокола DNS" - -msgid "Select the DNS protocol type for the domain resolver" -msgstr "Выберите тип протокола DNS для резолвера доменов" - -msgid "DNS over HTTPS (DoH)" -msgstr "DNS через HTTPS (DoH)" - -msgid "DNS over TLS (DoT)" -msgstr "DNS через TLS (DoT)" - -msgid "UDP (Unprotected DNS)" -msgstr "UDP (Незащищённый DNS)" - -msgid "DNS Server" -msgstr "DNS-сервер" - -msgid "Select or enter DNS server address" -msgstr "Выберите или введите адрес DNS-сервера" - -msgid "Community Lists" -msgstr "Списки сообщества" - -msgid "Service List" -msgstr "Список сервисов" - -msgid "Select predefined service for routing" -msgstr "Выберите предустановленные сервисы для маршрутизации" - -msgid "Regional options cannot be used together" -msgstr "Нельзя использовать несколько региональных опций одновременно" - -#, javascript-format -msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." -msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." - -msgid "Russia inside restrictions" -msgstr "Ограничения Russia inside" - -#, javascript-format -msgid "" -"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "" -"Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора." - -msgid "User Domain List Type" -msgstr "Тип пользовательского списка доменов" - -msgid "Select how to add your custom domains" -msgstr "Выберите способ добавления пользовательских доменов" - -msgid "Disabled" -msgstr "Отключено" - -msgid "Dynamic List" -msgstr "Динамический список" - -msgid "Text List" -msgstr "Текстовый список" - -msgid "User Domains" -msgstr "Пользовательские домены" - -msgid "Enter domain names without protocols (example: sub.example.com or example.com)" -msgstr "Введите доменные имена без протоколов (например: sub.example.com или example.com)" - -msgid "User Domains List" -msgstr "Список пользовательских доменов" - -msgid "Enter domain names separated by comma, space or newline. You can add comments after //" -msgstr "Введите домены через запятую, пробел или с новой строки. Можно добавлять комментарии после //" - -msgid "At least one valid domain must be specified. Comments-only content is not allowed." -msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается." - -msgid "Validation errors:" -msgstr "Ошибки валидации:" - -msgid "Local Domain Lists" -msgstr "Локальные списки доменов" - -msgid "Use the list from the router filesystem" -msgstr "Использовать список из файловой системы роутера" - -msgid "Local Domain List Paths" -msgstr "Пути к локальным спискам доменов" - -msgid "Enter the list file path" -msgstr "Введите путь к файлу списка" - -msgid "Remote Domain Lists" -msgstr "Удалённые списки доменов" - -msgid "Download and use domain lists from remote URLs" -msgstr "Загружать и использовать списки доменов с удалённых URL" - -msgid "Remote Domain URLs" -msgstr "URL удалённых доменов" - -msgid "Enter full URLs starting with http:// or https://" -msgstr "Введите полные URL, начинающиеся с http:// или https://" - -msgid "Local Subnet Lists" -msgstr "Локальные списки подсетей" - -msgid "Local Subnet List Paths" -msgstr "Пути к локальным спискам подсетей" - -msgid "User Subnet List Type" -msgstr "Тип пользовательского списка подсетей" - -msgid "Select how to add your custom subnets" -msgstr "Выберите способ добавления пользовательских подсетей" - -msgid "Text List (comma/space/newline separated)" -msgstr "Текстовый список (через запятую, пробел или новую строку)" - -msgid "User Subnets" -msgstr "Пользовательские подсети" - -msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses" -msgstr "Введите подсети в нотации CIDR (например: 103.21.244.0/22) или отдельные IP-адреса" - -msgid "User Subnets List" -msgstr "Список пользовательских подсетей" - -msgid "" -"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments " -"after //" -msgstr "" -"Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии " -"после //" - -msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." -msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." - -msgid "Remote Subnet Lists" -msgstr "Удалённые списки подсетей" - -msgid "Download and use subnet lists from remote URLs" -msgstr "Загружать и использовать списки подсетей с удалённых URL" - -msgid "Remote Subnet URLs" -msgstr "URL удалённых подсетей" - -msgid "IP for full redirection" -msgstr "IP для полного перенаправления" - -msgid "Specify local IP addresses whose traffic will always use the configured route" -msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут" - -msgid "Local IPs" -msgstr "Локальные IP-адреса" - -msgid "Enter valid IPv4 addresses" -msgstr "Введите действительные IPv4-адреса" - -msgid "Extra configurations" -msgstr "Дополнительные конфигурации" - -msgid "Add Section" -msgstr "Добавить раздел" - -msgid "Dashboard" -msgstr "Дашборд" - -msgid "Valid" -msgstr "Валидно" - -msgid "Invalid IP address" -msgstr "Неверный IP-адрес" - -msgid "Invalid domain address" -msgstr "Неверный домен" - -msgid "DNS server address cannot be empty" -msgstr "Адрес DNS-сервера не может быть пустым" - -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH" - -msgid "URL must use one of the following protocols:" -msgstr "URL должен использовать один из следующих протоколов:" - -msgid "Invalid URL format" -msgstr "Неверный формат URL" - -msgid "Path cannot be empty" -msgstr "Путь не может быть пустым" - -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы" - -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" - -msgid "IP address 0.0.0.0 is not allowed" -msgstr "IP-адрес 0.0.0.0 не допускается" - -msgid "CIDR must be between 0 and 32" -msgstr "CIDR должен быть между 0 и 32" - -msgid "Invalid Shadowsocks URL: must start with ss://" -msgstr "Неверный URL Shadowsocks: должен начинаться с ss://" - -msgid "Invalid Shadowsocks URL: must not contain spaces" -msgstr "Неверный URL Shadowsocks: не должен содержать пробелов" - -msgid "Invalid Shadowsocks URL: missing credentials" -msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные" - -msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" -msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password" - -msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" -msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" - -msgid "Invalid Shadowsocks URL: missing server address" -msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера" - -msgid "Invalid Shadowsocks URL: missing server" -msgstr "Неверный URL Shadowsocks: отсутствует сервер" - -msgid "Invalid Shadowsocks URL: missing port" -msgstr "Неверный URL Shadowsocks: отсутствует порт" - -msgid "Invalid port number. Must be between 1 and 65535" -msgstr "Неверный номер порта. Допустимо от 1 до 65535" - -msgid "Invalid Shadowsocks URL: parsing failed" -msgstr "Неверный URL Shadowsocks: ошибка разбора" - -msgid "Invalid VLESS URL: must not contain spaces" -msgstr "Неверный URL VLESS: не должен содержать пробелов" - -msgid "Invalid VLESS URL: must start with vless://" -msgstr "Неверный URL VLESS: должен начинаться с vless://" - -msgid "Invalid VLESS URL: missing UUID" -msgstr "Неверный URL VLESS: отсутствует UUID" - -msgid "Invalid VLESS URL: missing server" -msgstr "Неверный URL VLESS: отсутствует сервер" - -msgid "Invalid VLESS URL: missing port" -msgstr "Неверный URL VLESS: отсутствует порт" - -msgid "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" -msgstr "Неверный URL VLESS: недопустимый порт (1–65535)" - -msgid "Invalid VLESS URL: missing query parameters" -msgstr "Неверный URL VLESS: отсутствуют параметры запроса" - -msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" -msgstr "Неверный URL VLESS: тип должен быть tcp, raw, udp, grpc, http или ws" - -msgid "Invalid VLESS URL: security must be one of tls, reality, none" -msgstr "Неверный URL VLESS: параметр security должен быть tls, reality или none" - -msgid "Invalid VLESS URL: missing pbk parameter for reality security" -msgstr "Неверный URL VLESS: отсутствует параметр pbk для security=reality" - -msgid "Invalid VLESS URL: missing fp parameter for reality security" -msgstr "Неверный URL VLESS: отсутствует параметр fp для security=reality" - -msgid "Invalid VLESS URL: parsing failed" -msgstr "Неверный URL VLESS: ошибка разбора" - -msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" -msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\"" - -msgid "Invalid JSON format" -msgstr "Неверный формат JSON" - -msgid "Invalid Trojan URL: must start with trojan://" -msgstr "Неверный URL Trojan: должен начинаться с trojan://" - -msgid "Invalid Trojan URL: must not contain spaces" -msgstr "Неверный URL Trojan: не должен содержать пробелов" - -msgid "Invalid Trojan URL: must contain username, hostname and port" -msgstr "Неверный URL Trojan: должен содержать имя пользователя, хост и порт" - -msgid "Invalid Trojan URL: parsing failed" -msgstr "Неверный URL Trojan: ошибка разбора" - -msgid "URL must start with vless:// or ss:// or trojan://" -msgstr "URL должен начинаться с vless://, ss:// или trojan://" - -msgid "Operation timed out" -msgstr "Время ожидания истекло" - -msgid "HTTP error" -msgstr "Ошибка HTTP" - -msgid "Unknown error" -msgstr "Неизвестная ошибка" - -msgid "Fastest" -msgstr "Самый быстрый" - -msgid "Dashboard currently unavailable" -msgstr "Дашборд сейчас недоступен" - -msgid "Currently unavailable" -msgstr "Временно недоступно" - -msgid "Traffic" -msgstr "Трафик" - -msgid "Uplink" -msgstr "Исходящий" - -msgid "Downlink" -msgstr "Входящий" - -msgid "Traffic Total" -msgstr "Всего трафика" - -msgid "System info" -msgstr "Системная информация" - -msgid "Active Connections" -msgstr "Активные соединения" - -msgid "Memory Usage" -msgstr "Использование памяти" - -msgid "Services info" -msgstr "Информация о сервисах" - -msgid "Podkop" -msgstr "Podkop" - msgid "✔ Enabled" msgstr "✔ Включено" -msgid "✘ Disabled" -msgstr "✘ Отключено" - -msgid "Sing-box" -msgstr "Sing-box" - msgid "✔ Running" msgstr "✔ Работает" +msgid "✘ Disabled" +msgstr "✘ Отключено" + msgid "✘ Stopped" msgstr "✘ Остановлен" -msgid "Copied!" -msgstr "Скопировано!" +msgid "Active Connections" +msgstr "Активные соединения" -msgid "Failed to copy: " -msgstr "Не удалось скопировать: " +msgid "Additional marking rules found" +msgstr "Найдены дополнительные правила маркировки" -msgid "Loading..." -msgstr "Загрузка..." +msgid "Applicable for SOCKS and Shadowsocks proxy" +msgstr "Применимо для SOCKS и Shadowsocks прокси" -msgid "Copy to Clipboard" -msgstr "Копировать в буфер" +msgid "At least one valid domain must be specified. Comments-only content is not allowed." +msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается." -msgid "Close" -msgstr "Закрыть" +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." +msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." -msgid "No output" -msgstr "Нет вывода" - -msgid "FakeIP is working in browser!" -msgstr "FakeIP работает в браузере!" - -msgid "FakeIP is not working in browser" -msgstr "FakeIP не работает в браузере" - -msgid "Check DNS server on current device (PC, phone)" -msgstr "Проверьте DNS-сервер на текущем устройстве (ПК, телефон)" - -msgid "Its must be router!" -msgstr "Это должен быть роутер!" - -msgid "Proxy working correctly" -msgstr "Прокси работает корректно" - -msgid "Direct IP: " -msgstr "Прямой IP: " - -msgid "Proxy IP: " -msgstr "Прокси IP: " - -msgid "Proxy is not working - same IP for both domains" -msgstr "Прокси не работает — одинаковый IP для обоих доменов" - -msgid "IP: " -msgstr "IP: " - -msgid "Proxy check failed" -msgstr "Проверка прокси не удалась" - -msgid "Check failed: " -msgstr "Проверка не удалась: " - -msgid "timeout" -msgstr "таймаут" - -msgid "Error: " -msgstr "Ошибка: " - -msgid "Podkop Status" -msgstr "Статус Podkop" - -msgid "Global check" -msgstr "Глобальная проверка" - -msgid "Click here for all the info" -msgstr "Нажмите для просмотра всей информации" - -msgid "Update Lists" -msgstr "Обновить списки" - -msgid "Lists Update Results" -msgstr "Результаты обновления списков" - -msgid "Sing-box Status" -msgstr "Статус Sing-box" - -msgid "Check NFT Rules" -msgstr "Проверить правила NFT" - -msgid "NFT Rules" -msgstr "Правила NFT" - -msgid "Check DNSMasq" -msgstr "Проверить DNSMasq" - -msgid "DNSMasq Configuration" -msgstr "Конфигурация DNSMasq" - -msgid "FakeIP Status" -msgstr "Статус FakeIP" - -msgid "DNS Status" -msgstr "Статус DNS" - -msgid "Main config" -msgstr "Основная конфигурация" - -msgid "Version Information" -msgstr "Информация о версии" - -msgid "Podkop: " -msgstr "Podkop: " - -msgid "LuCI App: " -msgstr "LuCI App: " - -msgid "Sing-box: " -msgstr "Sing-box: " - -msgid "OpenWrt Version: " -msgstr "Версия OpenWrt: " - -msgid "Device Model: " -msgstr "Модель устройства: " - -msgid "Unknown" -msgstr "Неизвестно" - -msgid "works in browser" -msgstr "работает в браузере" - -msgid "does not work in browser" -msgstr "не работает в браузере" - -msgid "works on router" -msgstr "работает на роутере" - -msgid "does not work on router" -msgstr "не работает на роутере" - -msgid "Config: " -msgstr "Конфигурация: " - -msgid "Diagnostics" -msgstr "Диагностика" - -msgid "Additional Settings" -msgstr "Дополнительные настройки" - -msgid "Yacd enable" -msgstr "Включить YACD" - -msgid "Exclude NTP" -msgstr "Исключить NTP" - -msgid "Allows you to exclude NTP protocol traffic from the tunnel" -msgstr "Позволяет исключить направление трафика NTP-протокола в туннель" - -msgid "QUIC disable" -msgstr "Отключить QUIC" - -msgid "For issues with the video stream" -msgstr "Для проблем с видеопотоком" - -msgid "List Update Frequency" -msgstr "Частота обновления списков" - -msgid "Select how often the lists will be updated" -msgstr "Выберите как часто будут обновляться списки" - -msgid "Select DNS protocol to use" -msgstr "Выберите протокол DNS" +msgid "Bootsrap DNS" +msgstr "Bootstrap DNS" msgid "Bootstrap DNS server" msgstr "Bootstrap DNS-сервер" -msgid "The DNS server used to look up the IP address of an upstream DNS server" -msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" +msgid "Browser is not using FakeIP" +msgstr "Браузер не использует FakeIP" -msgid "DNS Rewrite TTL" -msgstr "Перезапись TTL для DNS" - -msgid "Time in seconds for DNS record caching (default: 60)" -msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" - -msgid "TTL value cannot be empty" -msgstr "Значение TTL не может быть пустым" - -msgid "TTL must be a positive number" -msgstr "TTL должно быть положительным числом" - -msgid "Config File Path" -msgstr "Путь к файлу конфигурации" - -msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" -msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" +msgid "Browser is using FakeIP correctly" +msgstr "Браузер использует FakeIP" msgid "Cache File Path" msgstr "Путь к файлу кэша" -msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" -msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" - msgid "Cache file path cannot be empty" msgstr "Путь к файлу кэша не может быть пустым" -msgid "Path must be absolute (start with /)" -msgstr "Путь должен быть абсолютным (начинаться с /)" +msgid "Cannot receive DNS checks result" +msgstr "Не удалось получить результаты проверки DNS" -msgid "Path must end with cache.db" -msgstr "Путь должен заканчиваться на cache.db" +msgid "Cannot receive nftables checks result" +msgstr "Не удалось получить результаты проверки nftables" -msgid "Path must contain at least one directory (like /tmp/cache.db)" -msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" +msgid "Cannot receive Sing-box checks result" +msgstr "Не удалось получить результаты проверки Sing-box" -msgid "Source Network Interface" -msgstr "Сетевой интерфейс источника" +msgid "Checking dns, please wait" +msgstr "Проверка dns, пожалуйста подождите" -msgid "Select the network interface from which the traffic will originate" -msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" +msgid "Checking FakeIP, please wait" +msgstr "Проверка FakeIP, пожалуйста подождите" -msgid "Interface monitoring" -msgstr "Мониторинг интерфейсов" +msgid "Checking nftables, please wait" +msgstr "Проверка nftables, пожалуйста подождите" -msgid "Interface monitoring for bad WAN" -msgstr "Мониторинг интерфейсов для плохого WAN" +msgid "Checking sing-box, please wait" +msgstr "Проверка sing-box, пожалуйста подождите" -msgid "Interface for monitoring" -msgstr "Интерфейс для мониторинга" +msgid "CIDR must be between 0 and 32" +msgstr "CIDR должен быть между 0 и 32" -msgid "Select the WAN interfaces to be monitored" -msgstr "Выберите WAN интерфейсы для мониторинга" +msgid "Close" +msgstr "Закрыть" -msgid "Interface Monitoring Delay" -msgstr "Задержка при мониторинге интерфейсов" +msgid "Community Lists" +msgstr "Списки сообщества" + +msgid "Config File Path" +msgstr "Путь к файлу конфигурации" + +msgid "Configuration for Podkop service" +msgstr "Настройки сервиса Podkop" + +msgid "Configuration Type" +msgstr "Тип конфигурации" + +msgid "Connection Type" +msgstr "Тип подключения" + +msgid "Connection URL" +msgstr "URL подключения" + +msgid "Copy" +msgstr "Копировать" + +msgid "Currently unavailable" +msgstr "Временно недоступно" + +msgid "Dashboard" +msgstr "Дашборд" + +msgid "Dashboard currently unavailable" +msgstr "Дашборд сейчас недоступен" msgid "Delay in milliseconds before reloading podkop after interface UP" msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" @@ -674,26 +125,590 @@ msgstr "Задержка в миллисекундах перед перезаг msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" -msgid "Dont touch my DHCP!" -msgstr "Не трогать мой DHCP!" +msgid "DHCP has DNS server" +msgstr "DHCP содержит DNS сервер" -msgid "Podkop will not change the DHCP config" -msgstr "Podkop не будет изменять конфигурацию DHCP" +msgid "Diagnostics" +msgstr "Диагностика" -msgid "Proxy download of lists" -msgstr "Загрузка списков через прокси" +msgid "Disable autostart" +msgstr "Отключить автостарт" + +msgid "Disable QUIC" +msgstr "Отключить QUIC" + +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "Отключить QUIC протокол для улучшения совместимости или исправления видео стриминга" + +msgid "Disabled" +msgstr "Отключено" + +msgid "DNS checks" +msgstr "DNS проверки" + +msgid "DNS checks passed" +msgstr "DNS проверки успешно завершены" + +msgid "DNS on router" +msgstr "DNS на роутере" + +msgid "DNS over HTTPS (DoH)" +msgstr "DNS через HTTPS (DoH)" + +msgid "DNS over TLS (DoT)" +msgstr "DNS через TLS (DoT)" + +msgid "DNS Protocol Type" +msgstr "Тип протокола DNS" + +msgid "DNS Rewrite TTL" +msgstr "Перезапись TTL для DNS" + +msgid "DNS Server" +msgstr "DNS-сервер" + +msgid "DNS server address cannot be empty" +msgstr "Адрес DNS-сервера не может быть пустым" + +msgid "Domain Resolver" +msgstr "Резолвер доменов" + +msgid "Dont Touch My DHCP!" +msgstr "Dont Touch My DHCP!" + +msgid "Downlink" +msgstr "Входящий" + +msgid "Download" +msgstr "Скачать" + +msgid "Download Lists via Proxy/VPN" +msgstr "Скачивать списки через Proxy/VPN" + +msgid "Download Lists via specific proxy section" +msgstr "Скачивать списки через выбранную секцию" msgid "Downloading all lists via main Proxy/VPN" msgstr "Загрузка всех списков через основной прокси/VPN" -msgid "IP for exclusion" -msgstr "IP для исключения" +msgid "Downloading all lists via specific Proxy/VPN" +msgstr "Загрузка всех списков через указанный прокси/VPN" -msgid "Specify local IP addresses that will never use the configured route" -msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут" +msgid "Dynamic List" +msgstr "Динамический список" -msgid "Mixed enable" -msgstr "Включить смешанный режим" +msgid "Enable autostart" +msgstr "Включить автостарт" -msgid "Browser port: 2080" -msgstr "Порт браузера: 2080" +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе" + +msgid "Enable Mixed Proxy" +msgstr "Включить смешанный прокси" + +msgid "Enable Output Network Interface" +msgstr "Включить выходной сетевой интерфейс" + +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "Включить смешанный прокси-сервер, разрешив этому разделу маршрутизировать трафик как через HTTP, так и через SOCKS-прокси." + +msgid "Enable YACD" +msgstr "Включить YACD" + +msgid "Enter complete outbound configuration in JSON format" +msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" + +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "Введите доменные имена, разделяя их запятыми, пробелами или переносами строк. Вы можете добавлять комментарии, используя //" + +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "Введите доменные имена без протоколов, например example.com или sub.example.com" + +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" + +msgid "Exclude NTP" +msgstr "Исключить NTP" + +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN." + +msgid "Failed to copy!" +msgstr "Не удалось скопировать!" + +msgid "FakeIP checks" +msgstr "Проверка FakeIP" + +msgid "FakeIP checks failed" +msgstr "Проверки FakeIP не пройдены" + +msgid "FakeIP checks partially passed" +msgstr "Проверка FakeIP частично пройдена" + +msgid "FakeIP checks passed" +msgstr "Проверки FakeIP пройдены" + +msgid "Fastest" +msgstr "Самый быстрый" + +msgid "Fully Routed IPs" +msgstr "Полностью маршрутизированные IP-адреса" + +msgid "Get global check" +msgstr "Получить глобальную проверку" + +msgid "Global check" +msgstr "Глобальная проверка" + +msgid "HTTP error" +msgstr "Ошибка HTTP" + +msgid "Interface Monitoring" +msgstr "Мониторинг интерфейса" + +msgid "Interface Monitoring Delay" +msgstr "Задержка при мониторинге интерфейсов" + +msgid "Interface monitoring for Bad WAN" +msgstr "Мониторинг интерфейса для Bad WAN" + +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH" + +msgid "Invalid domain address" +msgstr "Неверный домен" + +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" + +msgid "Invalid IP address" +msgstr "Неверный IP-адрес" + +msgid "Invalid JSON format" +msgstr "Неверный формат JSON" + +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы" + +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "Неверный номер порта. Допустимо от 1 до 65535" + +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password" + +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные" + +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" + +msgid "Invalid Shadowsocks URL: missing port" +msgstr "Неверный URL Shadowsocks: отсутствует порт" + +msgid "Invalid Shadowsocks URL: missing server" +msgstr "Неверный URL Shadowsocks: отсутствует сервер" + +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера" + +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "Неверный URL Shadowsocks: не должен содержать пробелов" + +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "Неверный URL Shadowsocks: должен начинаться с ss://" + +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "Неверный URL Shadowsocks: ошибка разбора" + +msgid "Invalid SOCKS URL: invalid host format" +msgstr "Неверный URL SOCKS: неверный формат хоста" + +msgid "Invalid SOCKS URL: invalid port number" +msgstr "Неверный URL SOCKS: неверный номер порта" + +msgid "Invalid SOCKS URL: missing host and port" +msgstr "Неверный URL SOCKS: отсутствует хост и порт" + +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "Неверный URL SOCKS: отсутствует имя хоста или IP-адрес" + +msgid "Invalid SOCKS URL: missing port" +msgstr "Неверный URL SOCKS: отсутствует порт" + +msgid "Invalid SOCKS URL: missing username" +msgstr "Неверный URL SOCKS: отсутствует имя пользователя" + +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "Неверный URL SOCKS: не должен содержать пробелов" + +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "Неверный URL-адрес SOCKS: должен начинаться с socks4://, socks4a:// или socks5://" + +msgid "Invalid SOCKS URL: parsing failed" +msgstr "Неверный URL SOCKS: парсинг не удался" + +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "Неверный URL Trojan: не должен содержать пробелов" + +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "Неверный URL Trojan: должен начинаться с trojan://" + +msgid "Invalid Trojan URL: parsing failed" +msgstr "Неверный URL Trojan: ошибка разбора" + +msgid "Invalid URL format" +msgstr "Неверный формат URL" + +msgid "Invalid VLESS URL: parsing failed" +msgstr "Неверный URL VLESS: ошибка разбора" + +msgid "IP address 0.0.0.0 is not allowed" +msgstr "IP-адрес 0.0.0.0 не допускается" + +msgid "Latest" +msgstr "Последняя" + +msgid "List Update Frequency" +msgstr "Частота обновления списков" + +msgid "Local Domain Lists" +msgstr "Локальные списки доменов" + +msgid "Local Subnet Lists" +msgstr "Локальные списки подсетей" + +msgid "Main DNS" +msgstr "Основной DNS" + +msgid "Memory Usage" +msgstr "Использование памяти" + +msgid "Mixed Proxy Port" +msgstr "Порт смешанного прокси" + +msgid "Monitored Interfaces" +msgstr "Наблюдаемые интерфейсы" + +msgid "Network Interface" +msgstr "Сетевой интерфейс" + +msgid "Nftables checks" +msgstr "Проверки Nftables" + +msgid "Nftables checks partially passed" +msgstr "Проверки Nftables частично пройдена" + +msgid "Nftables checks passed" +msgstr "Nftables проверки успешно завершены" + +msgid "No other marking rules found" +msgstr "Другие правила маркировки не найдены" + +msgid "Not implement yet" +msgstr "Ещё не реализовано" + +msgid "Not running" +msgstr "Не запущено" + +msgid "Operation timed out" +msgstr "Время ожидания истекло" + +msgid "Outbound Config" +msgstr "Конфигурация Outbound" + +msgid "Outbound Configuration" +msgstr "Конфигурация исходящего соединения" + +msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" +msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\"" + +msgid "Outdated" +msgstr "Устаревшая" + +msgid "Output Network Interface" +msgstr "Выходной сетевой интерфейс" + +msgid "Path cannot be empty" +msgstr "Путь не может быть пустым" + +msgid "Path must be absolute (start with /)" +msgstr "Путь должен быть абсолютным (начинаться с /)" + +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" + +msgid "Path must end with cache.db" +msgstr "Путь должен заканчиваться на cache.db" + +msgid "Podkop" +msgstr "Podkop" + +msgid "Podkop Settings" +msgstr "Настройки podkop" + +msgid "Podkop will not modify your DHCP configuration" +msgstr "Podkop не будет изменять вашу конфигурацию DHCP." + +msgid "Proxy Configuration URL" +msgstr "URL конфигурации прокси" + +msgid "Proxy traffic is not routed via FakeIP" +msgstr "Прокси-трафик не маршрутизируется через FakeIP" + +msgid "Proxy traffic is routed via FakeIP" +msgstr "Прокси-трафик направляется через FakeIP" + +msgid "Queued" +msgstr "В очереди" + +msgid "Regional options cannot be used together" +msgstr "Нельзя использовать несколько региональных опций одновременно" + +msgid "Remote Domain Lists" +msgstr "Удалённые списки доменов" + +msgid "Remote Subnet Lists" +msgstr "Удалённые списки подсетей" + +msgid "Restart podkop" +msgstr "Перезапустить Podkop" + +msgid "Router DNS is not routed through sing-box" +msgstr "DNS роутера не проходит через sing-box" + +msgid "Router DNS is routed through sing-box" +msgstr "DNS роутера проходит через sing-box" + +msgid "Routing Excluded IPs" +msgstr "Исключённые из маршрутизации IP-адреса" + +msgid "Rules mangle counters" +msgstr "Счётчики правил mangle" + +msgid "Rules mangle exist" +msgstr "Правила mangle существуют" + +msgid "Rules mangle output counters" +msgstr "Счётчики правил mangle output" + +msgid "Rules mangle output exist" +msgstr "Правила mangle output существуют" + +msgid "Rules proxy counters" +msgstr "Счётчики правил proxy" + +msgid "Rules proxy exist" +msgstr "Правила прокси существуют" + +msgid "Run Diagnostic" +msgstr "Запустить диагностику" + +msgid "Russia inside restrictions" +msgstr "Ограничения Russia inside" + +msgid "Sections" +msgstr "Секции" + +msgid "Select a predefined list for routing" +msgstr "Выберите предопределенный список для маршрутизации" + +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика" + +msgid "Select DNS protocol to use" +msgstr "Выберите протокол DNS" + +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "Выберите частоту автоматического обновления списков доменов или подсетей." + +msgid "Select how to configure the proxy" +msgstr "Выберите способ настройки прокси" + +msgid "Select network interface for VPN connection" +msgstr "Выберите сетевой интерфейс для VPN подключения" + +msgid "Select or enter DNS server address" +msgstr "Выберите или введите адрес DNS-сервера" + +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Select the DNS protocol type for the domain resolver" +msgstr "Выберите тип протокола DNS для резолвера доменов" + +msgid "Select the list type for adding custom domains" +msgstr "Выберите тип списка для добавления пользовательских доменов" + +msgid "Select the list type for adding custom subnets" +msgstr "Выберите тип списка для добавления пользовательских подсетей" + +msgid "Select the network interface from which the traffic will originate" +msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" + +msgid "Select the network interface to which the traffic will originate" +msgstr "Выберите сетевой интерфейс, на который будет поступать трафик." + +msgid "Select the WAN interfaces to be monitored" +msgstr "Выберите WAN интерфейсы для мониторинга" + +msgid "Services info" +msgstr "Информация о сервисах" + +msgid "Settings" +msgstr "Настройки" + +msgid "Show sing-box config" +msgstr "Показать sing-box конфигурацию" + +msgid "Sing-box" +msgstr "Sing-box" + +msgid "Sing-box autostart disabled" +msgstr "Автостарт sing-box отключен" + +msgid "Sing-box checks" +msgstr "Sing-box проверки" + +msgid "Sing-box checks passed" +msgstr "Sing-box проверки успешно завершены" + +msgid "Sing-box installed" +msgstr "Sing-box установлен" + +msgid "Sing-box listening ports" +msgstr "Sing-box слушает порты" + +msgid "Sing-box process running" +msgstr "Процесс sing-box запущен" + +msgid "Sing-box service exist" +msgstr "Сервис sing-box существует" + +msgid "Sing-box version >= 1.12.4" +msgstr "Версия sing-box >= 1.12.4" + +msgid "Source Network Interface" +msgstr "Сетевой интерфейс источника" + +msgid "Specify a local IP address to be excluded from routing" +msgstr "Укажите локальный IP-адрес, который следует исключить из маршрутизации." + +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут." + +msgid "Specify remote URLs to download and use domain lists" +msgstr "Укажите удаленные URL-адреса для загрузки и использования списков доменов." + +msgid "Specify remote URLs to download and use subnet lists" +msgstr "Укажите удаленные URL-адреса для загрузки и использования списков подсетей." + +msgid "Specify the path to the list file located on the router filesystem" +msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." + +msgid "Start podkop" +msgstr "Запустить podkop" + +msgid "Stop podkop" +msgstr "Остановить podkop" + +msgid "Successfully copied!" +msgstr "Успешно скопировано!" + +msgid "System info" +msgstr "Системная информация" + +msgid "Table exist" +msgstr "Таблица существует" + +msgid "Test latency" +msgstr "Измерить задержки" + +msgid "Text List" +msgstr "Текстовый список" + +msgid "Text List (comma/space/newline separated)" +msgstr "Текстовый список (через запятую, пробел или новую строку)" + +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" + +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" + +msgid "Traffic" +msgstr "Трафик" + +msgid "Traffic Total" +msgstr "Всего трафика" + +msgid "TTL must be a positive number" +msgstr "TTL должно быть положительным числом" + +msgid "TTL value cannot be empty" +msgstr "Значение TTL не может быть пустым" + +msgid "UDP (Unprotected DNS)" +msgstr "UDP (Незащищённый DNS)" + +msgid "UDP over TCP" +msgstr "UDP через TCP" + +msgid "unknown" +msgstr "неизвестно" + +msgid "Unknown error" +msgstr "Неизвестная ошибка" + +msgid "Uplink" +msgstr "Исходящий" + +msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://" + +msgid "URL must use one of the following protocols:" +msgstr "URL должен использовать один из следующих протоколов:" + +msgid "URLTest" +msgstr "URLTest" + +msgid "URLTest Proxy Links" +msgstr "Ссылки прокси для URLTest" + +msgid "User Domain List Type" +msgstr "Тип пользовательского списка доменов" + +msgid "User Domains" +msgstr "Пользовательские домены" + +msgid "User Domains List" +msgstr "Список пользовательских доменов" + +msgid "User Subnet List Type" +msgstr "Тип пользовательского списка подсетей" + +msgid "User Subnets" +msgstr "Пользовательские подсети" + +msgid "User Subnets List" +msgstr "Список пользовательских подсетей" + +msgid "Valid" +msgstr "Валидно" + +msgid "Validation errors:" +msgstr "Ошибки валидации:" + +msgid "View logs" +msgstr "Посмотреть логи" + +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." + +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." + +msgid "You can select Output Network Interface, by default autodetect" +msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index 778c412..75fe1bb 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -1,954 +1,984 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# FIRST AUTHOR , YEAR. -# +# divocat , 2025. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-07 16:55+0300\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2025-10-21 20:02+0300\n" +"PO-Revision-Date: 2025-10-21 20:02+0300\n" +"Last-Translator: divocat \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: htdocs/luci-static/resources/view/podkop/configSection.js:12 -msgid "Basic Settings" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:18 -msgid "Connection Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:19 -msgid "Select between VPN and Proxy connection methods for traffic routing" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:30 -msgid "Configuration Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:31 -msgid "Select how to configure the proxy" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:33 -msgid "Connection URL" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:34 -msgid "Outbound Config" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:35 -msgid "URLTest" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:44 -msgid "Proxy Configuration URL" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:81 -msgid "Current config: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:88 -#: htdocs/luci-static/resources/view/podkop/configSection.js:96 -#: htdocs/luci-static/resources/view/podkop/configSection.js:106 -msgid "Config without description" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:115 -msgid "" -"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " -"configs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:139 -msgid "No active configuration found. One configuration is required." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:145 -msgid "Multiply active configurations found. Please leave one configuration." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:157 -msgid "Invalid URL format:" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:165 -msgid "Outbound Configuration" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:166 -msgid "Enter complete outbound configuration in JSON format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:190 -msgid "URLTest Proxy Links" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:214 -msgid "Shadowsocks UDP over TCP" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:215 -msgid "Apply for SS2022" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:226 -msgid "Network Interface" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:227 -msgid "Select network interface for VPN connection" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:274 -msgid "Domain Resolver" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:275 -msgid "Enable built-in DNS resolver for domains handled by this section" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:286 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:61 -msgid "DNS Protocol Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:287 -msgid "Select the DNS protocol type for the domain resolver" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:289 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:64 -msgid "DNS over HTTPS (DoH)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:290 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:65 -msgid "DNS over TLS (DoT)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:291 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:66 -msgid "UDP (Unprotected DNS)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:301 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:75 -msgid "DNS Server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:302 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:76 -msgid "Select or enter DNS server address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:325 -msgid "Community Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:335 -msgid "Service List" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:336 -msgid "Select predefined service for routing" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:372 -msgid "Regional options cannot be used together" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:375 -#, javascript-format -msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:391 -msgid "Russia inside restrictions" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:394 -#, javascript-format -msgid "" -"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:427 -msgid "User Domain List Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:428 -msgid "Select how to add your custom domains" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:430 -#: htdocs/luci-static/resources/view/podkop/configSection.js:625 -msgid "Disabled" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:431 -#: htdocs/luci-static/resources/view/podkop/configSection.js:626 -msgid "Dynamic List" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:432 -msgid "Text List" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:441 -msgid "User Domains" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:443 -msgid "Enter domain names without protocols (example: sub.example.com or example.com)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:469 -msgid "User Domains List" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:471 -msgid "Enter domain names separated by comma, space or newline. You can add comments after //" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:490 -msgid "At least one valid domain must be specified. Comments-only content is not allowed." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:501 -#: htdocs/luci-static/resources/view/podkop/configSection.js:696 -msgid "Validation errors:" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:511 -msgid "Local Domain Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:512 -#: htdocs/luci-static/resources/view/podkop/configSection.js:586 -msgid "Use the list from the router filesystem" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:522 -msgid "Local Domain List Paths" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:523 -#: htdocs/luci-static/resources/view/podkop/configSection.js:597 -msgid "Enter the list file path" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:548 -msgid "Remote Domain Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:549 -msgid "Download and use domain lists from remote URLs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:559 -msgid "Remote Domain URLs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:560 -#: htdocs/luci-static/resources/view/podkop/configSection.js:718 -msgid "Enter full URLs starting with http:// or https://" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:585 -msgid "Local Subnet Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:596 -msgid "Local Subnet List Paths" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:622 -msgid "User Subnet List Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:623 -msgid "Select how to add your custom subnets" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:627 -msgid "Text List (comma/space/newline separated)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:636 -msgid "User Subnets" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:638 -msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:664 -msgid "User Subnets List" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:666 -msgid "" -"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments " -"after //" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:685 -msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:706 -msgid "Remote Subnet Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:707 -msgid "Download and use subnet lists from remote URLs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:717 -msgid "Remote Subnet URLs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:743 -msgid "IP for full redirection" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:745 -msgid "Specify local IP addresses whose traffic will always use the configured route" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:756 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:326 -msgid "Local IPs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:757 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:327 -msgid "Enter valid IPv4 addresses" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/podkop.js:64 -msgid "Extra configurations" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/podkop.js:68 -msgid "Add Section" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/dashboardTab.js:11 -msgid "Dashboard" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:11 htdocs/luci-static/resources/view/podkop/main.js:28 -#: htdocs/luci-static/resources/view/podkop/main.js:37 htdocs/luci-static/resources/view/podkop/main.js:40 -#: htdocs/luci-static/resources/view/podkop/main.js:60 htdocs/luci-static/resources/view/podkop/main.js:115 -#: htdocs/luci-static/resources/view/podkop/main.js:204 htdocs/luci-static/resources/view/podkop/main.js:295 -#: htdocs/luci-static/resources/view/podkop/main.js:313 htdocs/luci-static/resources/view/podkop/main.js:346 -msgid "Valid" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:13 -msgid "Invalid IP address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:20 htdocs/luci-static/resources/view/podkop/main.js:26 -msgid "Invalid domain address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:34 -msgid "DNS server address cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:45 -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:57 -msgid "URL must use one of the following protocols:" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:62 -msgid "Invalid URL format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:71 -msgid "Path cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:84 -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:95 -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:100 -msgid "IP address 0.0.0.0 is not allowed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:111 -msgid "CIDR must be between 0 and 32" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:132 -msgid "Invalid Shadowsocks URL: must start with ss://" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:139 -msgid "Invalid Shadowsocks URL: must not contain spaces" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:147 -msgid "Invalid Shadowsocks URL: missing credentials" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:156 -msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:165 -msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:174 -msgid "Invalid Shadowsocks URL: missing server address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:181 -msgid "Invalid Shadowsocks URL: missing server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:188 -msgid "Invalid Shadowsocks URL: missing port" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:195 -msgid "Invalid port number. Must be between 1 and 65535" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:201 -msgid "Invalid Shadowsocks URL: parsing failed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:214 -msgid "Invalid VLESS URL: must not contain spaces" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:220 -msgid "Invalid VLESS URL: must start with vless://" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:224 -msgid "Invalid VLESS URL: missing UUID" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:227 -msgid "Invalid VLESS URL: missing server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:230 -msgid "Invalid VLESS URL: missing port" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:236 -msgid "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:243 -msgid "Invalid VLESS URL: missing query parameters" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:263 -msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:273 -msgid "Invalid VLESS URL: security must be one of tls, reality, none" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:282 -msgid "Invalid VLESS URL: missing pbk parameter for reality security" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:290 -msgid "Invalid VLESS URL: missing fp parameter for reality security" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:297 -msgid "Invalid VLESS URL: parsing failed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:309 -msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:315 -msgid "Invalid JSON format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:324 -msgid "Invalid Trojan URL: must start with trojan://" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:330 -msgid "Invalid Trojan URL: must not contain spaces" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:339 -msgid "Invalid Trojan URL: must contain username, hostname and port" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:344 -msgid "Invalid Trojan URL: parsing failed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:362 -msgid "URL must start with vless:// or ss:// or trojan://" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:568 -msgid "Operation timed out" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:775 -msgid "HTTP error" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:786 -msgid "Unknown error" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:951 -msgid "Fastest" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1222 -msgid "Dashboard currently unavailable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1326 -msgid "Currently unavailable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1721 -msgid "Traffic" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1723 htdocs/luci-static/resources/view/podkop/main.js:1748 -msgid "Uplink" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1724 htdocs/luci-static/resources/view/podkop/main.js:1752 -msgid "Downlink" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1745 -msgid "Traffic Total" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1775 -msgid "System info" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1778 -msgid "Active Connections" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1782 -msgid "Memory Usage" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1805 -msgid "Services info" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1808 htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1139 -msgid "Podkop" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1809 +#: src/podkop/tabs/dashboard/initController.ts:342 msgid "✔ Enabled" msgstr "" -#: htdocs/luci-static/resources/view/podkop/main.js:1809 -msgid "✘ Disabled" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1815 -msgid "Sing-box" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/main.js:1816 +#: src/podkop/tabs/dashboard/initController.ts:353 msgid "✔ Running" msgstr "" -#: htdocs/luci-static/resources/view/podkop/main.js:1816 +#: src/podkop/tabs/dashboard/initController.ts:343 +msgid "✘ Disabled" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:354 msgid "✘ Stopped" msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:137 -msgid "Copied!" +#: src/podkop/tabs/dashboard/initController.ts:304 +msgid "Active Connections" msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:143 -msgid "Failed to copy: " +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:117 +msgid "Additional marking rules found" msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:327 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:542 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:759 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:762 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:765 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:768 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:771 -msgid "Loading..." +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111 +msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:351 -msgid "Copy to Clipboard" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:356 +msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:359 -msgid "Close" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:437 +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:380 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:483 -msgid "No output" +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72 +msgid "Bootsrap DNS" msgstr "" -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:398 -msgid "FakeIP is working in browser!" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:401 -msgid "FakeIP is not working in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:403 -msgid "Check DNS server on current device (PC, phone)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:404 -msgid "Its must be router!" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:426 -msgid "Proxy working correctly" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:428 -msgid "Direct IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:430 -msgid "Proxy IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:434 -msgid "Proxy is not working - same IP for both domains" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:437 -msgid "IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:440 -msgid "Proxy check failed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:448 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:459 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:470 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:477 -msgid "Check failed: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:450 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:461 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:471 -msgid "timeout" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:488 -msgid "Error: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:571 -msgid "Podkop Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:604 -msgid "Global check" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:606 -msgid "Click here for all the info" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:614 -msgid "Update Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:616 -msgid "Lists Update Results" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:633 -msgid "Sing-box Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:660 -msgid "Check NFT Rules" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:662 -msgid "NFT Rules" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:665 -msgid "Check DNSMasq" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:667 -msgid "DNSMasq Configuration" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:684 -msgid "FakeIP Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:711 -msgid "DNS Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:728 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1096 -msgid "Main config" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:748 -msgid "Version Information" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:758 -msgid "Podkop: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:761 -msgid "LuCI App: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:764 -msgid "Sing-box: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:767 -msgid "OpenWrt Version: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:770 -msgid "Device Model: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:916 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:929 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:943 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:962 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:964 -msgid "Unknown" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:988 -msgid "works in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:989 -msgid "does not work in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1014 -msgid "works on router" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1015 -msgid "does not work on router" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1110 -msgid "Config: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:1127 -msgid "Diagnostics" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:8 -msgid "Additional Settings" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:14 -msgid "Yacd enable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:25 -msgid "Exclude NTP" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:26 -msgid "Allows you to exclude NTP protocol traffic from the tunnel" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:36 -msgid "QUIC disable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:37 -msgid "For issues with the video stream" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:47 -msgid "List Update Frequency" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:48 -msgid "Select how often the lists will be updated" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:62 -msgid "Select DNS protocol to use" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:98 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45 msgid "Bootstrap DNS server" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:100 -msgid "The DNS server used to look up the IP address of an upstream DNS server" +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:81 +msgid "Browser is not using FakeIP" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:123 -msgid "DNS Rewrite TTL" +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:80 +msgid "Browser is using FakeIP correctly" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:124 -msgid "Time in seconds for DNS record caching (default: 60)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:131 -msgid "TTL value cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:136 -msgid "TTL must be a positive number" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:146 -msgid "Config File Path" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:148 -msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:161 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:322 msgid "Cache File Path" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:163 -msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:176 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336 msgid "Cache file path cannot be empty" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:180 -msgid "Path must be absolute (start with /)" +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:26 +msgid "Cannot receive DNS checks result" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:184 -msgid "Path must end with cache.db" +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:27 +msgid "Cannot receive nftables checks result" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:189 -msgid "Path must contain at least one directory (like /tmp/cache.db)" +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:24 +msgid "Cannot receive Sing-box checks result" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:199 -msgid "Source Network Interface" +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:14 +msgid "Checking dns, please wait" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:200 -msgid "Select the network interface from which the traffic will originate" +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:14 +msgid "Checking FakeIP, please wait" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:238 -msgid "Interface monitoring" +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:12 +msgid "Checking nftables, please wait" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:239 -msgid "Interface monitoring for bad WAN" +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:12 +msgid "Checking sing-box, please wait" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:249 -msgid "Interface for monitoring" +#: src/validators/validateSubnet.ts:33 +msgid "CIDR must be between 0 and 32" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:250 -msgid "Select the WAN interfaces to be monitored" +#: src/partials/modal/renderModal.ts:26 +msgid "Close" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:274 -msgid "Interface Monitoring Delay" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:211 +msgid "Community Lists" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:275 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:309 +msgid "Config File Path" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27 +msgid "Configuration for Podkop service" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:22 +msgid "Configuration Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12 +msgid "Connection Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:25 +msgid "Connection URL" +msgstr "" + +#: src/partials/modal/renderModal.ts:20 +msgid "Copy" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22 +msgid "Currently unavailable" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80 +msgid "Dashboard" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderSections.ts:19 +msgid "Dashboard currently unavailable" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:215 msgid "Delay in milliseconds before reloading podkop after interface UP" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:283 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222 msgid "Delay value cannot be empty" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:292 -msgid "Dont touch my DHCP!" +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:89 +msgid "DHCP has DNS server" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:293 -msgid "Podkop will not change the DHCP config" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65 +msgid "Diagnostics" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:303 -msgid "Proxy download of lists" +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79 +msgid "Disable autostart" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:304 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:239 +msgid "Disable QUIC" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:240 +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:302 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:382 +msgid "Disabled" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:14 +msgid "DNS checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:64 +msgid "DNS checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:84 +msgid "DNS on router" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:179 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15 +msgid "DNS over HTTPS (DoH)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:180 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16 +msgid "DNS over TLS (DoT)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:176 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12 +msgid "DNS Protocol Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68 +msgid "DNS Rewrite TTL" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:189 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24 +msgid "DNS Server" +msgstr "" + +#: src/validators/validateDns.ts:7 +msgid "DNS server address cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:166 +msgid "Domain Resolver" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:300 +msgid "Dont Touch My DHCP!" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:238 +#: src/podkop/tabs/dashboard/initController.ts:272 +msgid "Downlink" +msgstr "" + +#: src/partials/modal/renderModal.ts:15 +msgid "Download" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:262 +msgid "Download Lists via Proxy/VPN" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:271 +msgid "Download Lists via specific proxy section" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:263 msgid "Downloading all lists via main Proxy/VPN" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:315 -msgid "IP for exclusion" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:272 +msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:316 -msgid "Specify local IP addresses that will never use the configured route" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:303 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:383 +msgid "Dynamic List" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:352 -msgid "Mixed enable" +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89 +msgid "Enable autostart" msgstr "" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:353 -msgid "Browser port: 2080" +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:167 +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:575 +msgid "Enable Mixed Proxy" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126 +msgid "Enable Output Network Interface" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:576 +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:230 +msgid "Enable YACD" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65 +msgid "Enter complete outbound configuration in JSON format" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:338 +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:312 +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392 +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:358 +msgid "Exclude NTP" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:359 +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "" + +#: src/helpers/copyToClipboard.ts:12 +msgid "Failed to copy!" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:29 +msgid "FakeIP checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57 +msgid "FakeIP checks failed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51 +msgid "FakeIP checks partially passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:44 +msgid "FakeIP checks passed" +msgstr "" + +#: src/podkop/methods/custom/getDashboardSections.ts:117 +msgid "Fastest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:550 +msgid "Fully Routed IPs" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98 +msgid "Get global check" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:218 +msgid "Global check" +msgstr "" + +#: src/podkop/api.ts:27 +msgid "HTTP error" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:182 +msgid "Interface Monitoring" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:214 +msgid "Interface Monitoring Delay" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:183 +msgid "Interface monitoring for Bad WAN" +msgstr "" + +#: src/validators/validateDns.ts:20 +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "" + +#: src/validators/validateDomain.ts:18 +#: src/validators/validateDomain.ts:27 +msgid "Invalid domain address" +msgstr "" + +#: src/validators/validateSubnet.ts:11 +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "" + +#: src/validators/validateIp.ts:11 +msgid "Invalid IP address" +msgstr "" + +#: src/validators/validateOutboundJson.ts:19 +msgid "Invalid JSON format" +msgstr "" + +#: src/validators/validatePath.ts:22 +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:85 +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:37 +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:27 +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:46 +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:76 +msgid "Invalid Shadowsocks URL: missing port" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:67 +msgid "Invalid Shadowsocks URL: missing server" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:58 +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:16 +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:8 +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "" + +#: src/validators/validateShadowsocksUrl.ts:91 +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "" + +#: src/validators/validateSocksUrl.ts:73 +msgid "Invalid SOCKS URL: invalid host format" +msgstr "" + +#: src/validators/validateSocksUrl.ts:63 +msgid "Invalid SOCKS URL: invalid port number" +msgstr "" + +#: src/validators/validateSocksUrl.ts:42 +msgid "Invalid SOCKS URL: missing host and port" +msgstr "" + +#: src/validators/validateSocksUrl.ts:51 +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "" + +#: src/validators/validateSocksUrl.ts:56 +msgid "Invalid SOCKS URL: missing port" +msgstr "" + +#: src/validators/validateSocksUrl.ts:34 +msgid "Invalid SOCKS URL: missing username" +msgstr "" + +#: src/validators/validateSocksUrl.ts:19 +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "" + +#: src/validators/validateSocksUrl.ts:10 +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "" + +#: src/validators/validateSocksUrl.ts:77 +msgid "Invalid SOCKS URL: parsing failed" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:15 +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:8 +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "" + +#: src/validators/validateTrojanUrl.ts:56 +msgid "Invalid Trojan URL: parsing failed" +msgstr "" + +#: src/validators/validateUrl.ts:18 +msgid "Invalid URL format" +msgstr "" + +#: src/validators/validateVlessUrl.ts:109 +msgid "Invalid VLESS URL: parsing failed" +msgstr "" + +#: src/validators/validateSubnet.ts:18 +msgid "IP address 0.0.0.0 is not allowed" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:404 +msgid "Latest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:250 +msgid "List Update Frequency" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458 +msgid "Local Domain Lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:481 +msgid "Local Subnet Lists" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:79 +msgid "Main DNS" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:308 +msgid "Memory Usage" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:586 +msgid "Mixed Proxy Port" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:191 +msgid "Monitored Interfaces" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:120 +msgid "Network Interface" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:24 +msgid "Nftables checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:75 +msgid "Nftables checks partially passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74 +msgid "Nftables checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:116 +msgid "No other marking rules found" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189 +msgid "Not implement yet" +msgstr "" + +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:55 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:63 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:71 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:79 +msgid "Not running" +msgstr "" + +#: src/helpers/withTimeout.ts:7 +msgid "Operation timed out" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26 +msgid "Outbound Config" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:64 +msgid "Outbound Configuration" +msgstr "" + +#: src/validators/validateOutboundJson.ts:11 +msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:394 +msgid "Outdated" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135 +msgid "Output Network Interface" +msgstr "" + +#: src/validators/validatePath.ts:7 +msgid "Path cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:340 +msgid "Path must be absolute (start with /)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349 +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:344 +msgid "Path must end with cache.db" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:340 +msgid "Podkop" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26 +msgid "Podkop Settings" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:301 +msgid "Podkop will not modify your DHCP configuration" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:34 +msgid "Proxy Configuration URL" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:89 +msgid "Proxy traffic is not routed via FakeIP" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:88 +msgid "Proxy traffic is routed via FakeIP" +msgstr "" + +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:95 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:103 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:111 +#: src/podkop/tabs/diagnostic/diagnostic.store.ts:119 +msgid "Queued" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:245 +msgid "Regional options cannot be used together" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:504 +msgid "Remote Domain Lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:527 +msgid "Remote Subnet Lists" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49 +msgid "Restart podkop" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:74 +msgid "Router DNS is not routed through sing-box" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:73 +msgid "Router DNS is routed through sing-box" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:369 +msgid "Routing Excluded IPs" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:90 +msgid "Rules mangle counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:85 +msgid "Rules mangle exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:100 +msgid "Rules mangle output counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:95 +msgid "Rules mangle output exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:110 +msgid "Rules proxy counters" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105 +msgid "Rules proxy exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15 +msgid "Run Diagnostic" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264 +msgid "Russia inside restrictions" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36 +msgid "Sections" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:212 +msgid "Select a predefined list for routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13 +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13 +msgid "Select DNS protocol to use" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:251 +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23 +msgid "Select how to configure the proxy" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:121 +msgid "Select network interface for VPN connection" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:190 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25 +msgid "Select or enter DNS server address" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:323 +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:310 +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:177 +msgid "Select the DNS protocol type for the domain resolver" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300 +msgid "Select the list type for adding custom domains" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:380 +msgid "Select the list type for adding custom subnets" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90 +msgid "Select the network interface from which the traffic will originate" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136 +msgid "Select the network interface to which the traffic will originate" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:192 +msgid "Select the WAN interfaces to be monitored" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:337 +msgid "Services info" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49 +msgid "Settings" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:278 +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116 +msgid "Show sing-box config" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:351 +msgid "Sing-box" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:86 +msgid "Sing-box autostart disabled" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/contstants.ts:19 +msgid "Sing-box checks" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:66 +msgid "Sing-box checks passed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:71 +msgid "Sing-box installed" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:96 +msgid "Sing-box listening ports" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:91 +msgid "Sing-box process running" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:81 +msgid "Sing-box service exist" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:76 +msgid "Sing-box version >= 1.12.4" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89 +msgid "Source Network Interface" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370 +msgid "Specify a local IP address to be excluded from routing" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:551 +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505 +msgid "Specify remote URLs to download and use domain lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:528 +msgid "Specify remote URLs to download and use subnet lists" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:459 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:482 +msgid "Specify the path to the list file located on the router filesystem" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69 +msgid "Start podkop" +msgstr "" + +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59 +msgid "Stop podkop" +msgstr "" + +#: src/helpers/copyToClipboard.ts:10 +msgid "Successfully copied!" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:301 +msgid "System info" +msgstr "" + +#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:80 +msgid "Table exist" +msgstr "" + +#: src/podkop/tabs/dashboard/partials/renderSections.ts:108 +msgid "Test latency" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:304 +msgid "Text List" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:384 +msgid "Text List (comma/space/newline separated)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69 +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:235 +msgid "Traffic" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:265 +msgid "Traffic Total" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80 +msgid "TTL must be a positive number" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75 +msgid "TTL value cannot be empty" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:181 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17 +msgid "UDP (Unprotected DNS)" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:110 +msgid "UDP over TCP" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:34 +#: src/podkop/tabs/diagnostic/initController.ts:35 +#: src/podkop/tabs/diagnostic/initController.ts:36 +#: src/podkop/tabs/diagnostic/initController.ts:37 +#: src/podkop/tabs/diagnostic/initController.ts:38 +#: src/podkop/tabs/diagnostic/initController.ts:39 +#: src/podkop/tabs/diagnostic/initController.ts:373 +msgid "unknown" +msgstr "" + +#: src/podkop/api.ts:40 +msgid "Unknown error" +msgstr "" + +#: src/podkop/tabs/dashboard/initController.ts:237 +#: src/podkop/tabs/dashboard/initController.ts:268 +msgid "Uplink" +msgstr "" + +#: src/validators/validateProxyUrl.ts:27 +msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +msgstr "" + +#: src/validators/validateUrl.ts:13 +msgid "URL must use one of the following protocols:" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27 +msgid "URLTest" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:87 +msgid "URLTest Proxy Links" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299 +msgid "User Domain List Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:311 +msgid "User Domains" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:337 +msgid "User Domains List" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:379 +msgid "User Subnet List Type" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391 +msgid "User Subnets" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:417 +msgid "User Subnets List" +msgstr "" + +#: src/validators/validateDns.ts:11 +#: src/validators/validateDns.ts:15 +#: src/validators/validateDomain.ts:13 +#: src/validators/validateDomain.ts:30 +#: src/validators/validateIp.ts:8 +#: src/validators/validateOutboundJson.ts:17 +#: src/validators/validatePath.ts:16 +#: src/validators/validateShadowsocksUrl.ts:95 +#: src/validators/validateSocksUrl.ts:80 +#: src/validators/validateSubnet.ts:38 +#: src/validators/validateTrojanUrl.ts:59 +#: src/validators/validateUrl.ts:16 +#: src/validators/validateVlessUrl.ts:107 +msgid "Valid" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:370 +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:449 +msgid "Validation errors:" +msgstr "" + +#: src/podkop/tabs/diagnostic/initController.ts:248 +#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107 +msgid "View logs" +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:247 +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:266 +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" + +#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127 +msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/podkop/Makefile b/podkop/Makefile index b4f4b84..7eebfb8 100644 --- a/podkop/Makefile +++ b/podkop/Makefile @@ -14,7 +14,7 @@ include $(INCLUDE_DIR)/package.mk define Package/podkop SECTION:=net CATEGORY:=Network - DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 + DEPENDS:=+sing-box-tiny +curl +jq +kmod-nft-tproxy +coreutils-base64 +bind-dig CONFLICTS:=https-dns-proxy nextdns luci-app-passwall luci-app-passwall2 TITLE:=Domain routing app URL:=https://podkop.net diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index 6e2269e..f7224fb 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -1,47 +1,39 @@ -config main 'main' - option mode 'proxy' - #option interface '' - option proxy_config_type 'url' - #option outbound_json '' - option proxy_string '' - option community_lists_enabled '1' - list community_lists 'russia_inside' - option user_domain_list_type 'disabled' - #list user_domains '' - #option user_domains_text '' - option local_domain_lists_enabled '0' - #list local_domain_lists '' - option remote_domain_lists_enabled '0' - #list remote_domain_lists '' - option user_subnet_list_type 'disable' - #list user_subnets '' - #option user_subnets_text '' - option local_subnet_lists_enabled '0' - #list local_subnet_lists '' - option remote_subnet_lists_enabled '0' - #list remote_subnet_lists '' - option all_traffic_from_ip_enabled '0' - #list all_traffic_ip '' - option exclude_from_ip_enabled '0' - #list exclude_traffic_ip '' - option yacd '0' - option socks5 '0' - option exclude_ntp '0' - option quic_disable '0' - option dont_touch_dhcp '0' - option update_interval '1d' - option dns_type 'udp' - option dns_server '8.8.8.8' - option split_dns_enabled '1' - option split_dns_type 'udp' - option split_dns_server '1.1.1.1' - option dns_rewrite_ttl '60' - option config_path '/etc/sing-box/config.json' - option cache_path '/tmp/sing-box/cache.db' - list iface 'br-lan' - option mon_restart_ifaces '0' - #list restart_ifaces 'wan' - option procd_reload_delay '2000' - option ss_uot '0' - option detour '0' - option shutdown_correctly '1' \ No newline at end of file +config settings 'settings' + option dns_type 'udp' + option dns_server '8.8.8.8' + option bootstrap_dns_server '77.88.8.8' + option dns_rewrite_ttl '60' + list source_network_interfaces 'br-lan' + option enable_output_network_interface '0' + #option output_network_interface 'wan' + option enable_badwan_interface_monitoring '0' + #list badwan_monitored_interfaces 'wan' + #option badwan_reload_delay '2000' + option enable_yacd '0' + option disable_quic '0' + option update_interval '1d' + option download_lists_via_proxy '0' + option dont_touch_dhcp '0' + option config_path '/etc/sing-box/config.json' + option cache_path '/tmp/sing-box/cache.db' + option exclude_ntp '0' + option shutdown_correctly '0' + #list routing_excluded_ips '192.168.1.3' + +config section 'main' + option connection_type 'proxy' + option proxy_config_type 'url' + option proxy_string '' + option enable_udp_over_tcp '0' + list community_lists 'russia_inside' + #option user_domain_list_type 'dynamic' + #list user_domains '2ip.ru' + #option user_subnet_list_type 'dynamic' + #list user_subnets '1.1.1.1' + #list local_domain_lists '/tmp/domains.lst' + #list local_subnet_lists '/tmp/subnets.lst' + #list remote_domain_lists 'https://example.com/domains.srs' + #list remote_subnet_lists 'https://example.com/subnets.srs' + #list fully_routed_ips '192.168.1.2' + #option mixed_proxy_enabled '1' + #option mixed_proxy_port '2080' \ No newline at end of file diff --git a/podkop/files/etc/init.d/podkop b/podkop/files/etc/init.d/podkop index 1e4c73d..73825ef 100755 --- a/podkop/files/etc/init.d/podkop +++ b/podkop/files/etc/init.d/podkop @@ -1,4 +1,5 @@ #!/bin/sh /etc/rc.common +# shellcheck disable=SC2034,SC2154 START=99 USE_PROCD=1 @@ -10,15 +11,16 @@ config_load "$NAME" start_service() { echo "Start podkop" - config_get mon_restart_ifaces "main" "mon_restart_ifaces" - config_get restart_ifaces "main" "restart_ifaces" + config_get enable_badwan_interface_monitoring "settings" "enable_badwan_interface_monitoring" + config_get badwan_monitored_interfaces "settings" "badwan_monitored_interfaces" - procd_open_instance - procd_set_param command /usr/bin/podkop start - [ "$mon_restart_ifaces" = "1" ] && [ -n "$restart_ifaces" ] && procd_set_param netdev $restart_ifaces - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_close_instance + procd_open_instance + procd_set_param command /usr/bin/podkop start + [ "$enable_badwan_interface_monitoring" = "1" ] && [ -n "$badwan_monitored_interfaces" ] && + procd_set_param netdev "$badwan_monitored_interfaces" + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance } stop_service() { @@ -32,19 +34,19 @@ reload_service() { service_triggers() { echo "service_triggers start" - config_get mon_restart_ifaces "main" "mon_restart_ifaces" - config_get restart_ifaces "main" "restart_ifaces" - config_get procd_reload_delay "main" "procd_reload_delay" "2000" + config_get enable_badwan_interface_monitoring "settings" "enable_badwan_interface_monitoring" + config_get badwan_monitored_interfaces "settings" "badwan_monitored_interfaces" + config_get badwan_reload_delay "settings" "badwan_reload_delay" "2000" - PROCD_RELOAD_DELAY=$procd_reload_delay + PROCD_RELOAD_DELAY=$badwan_reload_delay - procd_open_trigger - procd_add_config_trigger "config.change" "$NAME" "$initscript" restart 'on_config_change' + procd_open_trigger + procd_add_config_trigger "config.change" "$NAME" "$initscript" restart 'on_config_change' - if [ "$mon_restart_ifaces" = "1" ]; then - for iface in $restart_ifaces; do - procd_add_interface_trigger "interface.*.up" "$iface" /etc/init.d/podkop reload - done - fi - procd_close_trigger -} \ No newline at end of file + if [ "$enable_badwan_interface_monitoring" = "1" ]; then + for iface in $badwan_monitored_interfaces; do + procd_add_interface_trigger "interface.*.up" "$iface" /etc/init.d/podkop reload + done + fi + procd_close_trigger +} diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 847e1b2..a87551b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -38,51 +38,70 @@ check_requirements() { coreutils_base64_version="$(base64 --version | head -n1 | awk '{print $4}')" if [ -z "$sing_box_version" ]; then - log "Package 'sing-box' is not installed." "error" + log "Package 'sing-box' is not installed. Aborted." "error" exit 1 else if ! is_min_package_version "$sing_box_version" "$SB_REQUIRED_VERSION"; then - log "Package 'sing-box' version ($sing_box_version) is lower than the required minimum ($SB_REQUIRED_VERSION). Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box" "error" + log "Package 'sing-box' version ($sing_box_version) is lower than the required minimum ($SB_REQUIRED_VERSION). Update sing-box: opkg update && opkg remove sing-box && opkg install sing-box. Aborted." "error" exit 1 fi if ! service_exists "sing-box"; then - log "Service 'sing-box' is missing. Please install the official package to ensure the service is available." "error" + log "Service 'sing-box' is missing. Please install the official package to ensure the service is available. Aborted." "error" exit 1 fi fi if [ -z "$jq_version" ]; then - log "Package 'jq' is not installed." "error" + log "Package 'jq' is not installed. Aborted." "error" exit 1 elif ! is_min_package_version "$jq_version" "$JQ_REQUIRED_VERSION"; then - log "Package 'jq' version ($jq_version) is lower than the required minimum ($JQ_REQUIRED_VERSION)." "error" + log "Package 'jq' version ($jq_version) is lower than the required minimum ($JQ_REQUIRED_VERSION). Aborted." "error" exit 1 fi if [ -z "$coreutils_base64_version" ]; then - log "Package 'coreutils-base64' is not installed." "error" + log "Package 'coreutils-base64' is not installed. Aborted." "error" exit 1 elif ! is_min_package_version "$coreutils_base64_version" "$COREUTILS_BASE64_REQUIRED_VERSION"; then log "Package 'coreutils-base64' version ($coreutils_base64_version) is lower than the required minimum ($COREUTILS_BASE64_REQUIRED_VERSION). This may cause issues when decoding base64 streams with missing padding, as automatic padding support is not available in older versions." "warn" fi if grep -qE 'doh_backup_noresolv|doh_backup_server|doh_server' /etc/config/dhcp; then - log "Detected https-dns-proxy in dhcp config. Edit /etc/config/dhcp" "warn" + log "Detected https-dns-proxy in DHCP config. Edit /etc/config/dhcp" "error" fi - local proxy_string interface outbound_json urltest_proxy_links dont_touch_dhcp - config_get proxy_string "main" "proxy_string" - config_get interface "main" "interface" - config_get outbound_json "main" "outbound_json" - config_get urltest_proxy_links "main" "urltest_proxy_links" - - if [ -z "$proxy_string" ] && [ -z "$interface" ] && [ -z "$outbound_json" ] && [ -z "$urltest_proxy_links" ]; then - log "Required options (proxy_string, interface, outbound_json, urltest_proxy_links) are missing in 'main' section. Aborted." "error" + if has_outbound_section; then + log "Outbound section found" "debug" + else + log "Outbound section not found. Please check your configuration file (missing proxy_string, interface, outbound_json, or urltest_proxy_links). Aborted." "error" exit 1 fi } +_check_outbound_section() { + local section="$1" + local proxy_string interface outbound_json urltest_proxy_links + + config_get proxy_string "$section" "proxy_string" + config_get interface "$section" "interface" + config_get outbound_json "$section" "outbound_json" + config_get urltest_proxy_links "$section" "urltest_proxy_links" + + if [ -n "$proxy_string" ] || [ -n "$interface" ] || + [ -n "$outbound_json" ] || [ -n "$urltest_proxy_links" ]; then + section_exists=0 + fi +} + +has_outbound_section() { + local section_exists=1 + + config_foreach _check_outbound_section "section" + + return $section_exists +} + start_main() { log "Starting podkop" @@ -90,7 +109,7 @@ start_main() { migration - config_foreach process_validate_service + config_foreach process_validate_service "section" br_netfilter_disable @@ -109,11 +128,11 @@ start_main() { # sing-box sing_box_init_config - config_foreach add_cron_job + config_foreach add_cron_job "section" /etc/init.d/sing-box start local exclude_ntp - config_get_bool exclude_ntp "main" "exclude_ntp" "0" + config_get_bool exclude_ntp "settings" "exclude_ntp" "0" if [ "$exclude_ntp" -eq 1 ]; then log "NTP traffic exclude for proxy" nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return @@ -126,11 +145,11 @@ start_main() { start() { start_main - config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0 + config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then dnsmasq_add_resolver fi - uci_set "podkop" "main" "shutdown_correctly" 0 + uci_set "podkop" "settings" "shutdown_correctly" 0 uci commit "podkop" && config_load "$PODKOP_CONFIG" } @@ -171,12 +190,12 @@ stop_main() { stop() { local dont_touch_dhcp - config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" 0 + config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then dnsmasq_restore fi stop_main - uci_set "podkop" "main" "shutdown_correctly" 1 + uci_set "podkop" "settings" "shutdown_correctly" 1 uci commit "podkop" && config_load "$PODKOP_CONFIG" } @@ -194,79 +213,7 @@ restart() { # Migrations and validation funcs migration() { - # list migrate - local CONFIG="/etc/config/podkop" - - if grep -q "ru_inside" $CONFIG; then - log "Deprecated list found: ru_inside" - sed -i '/ru_inside/d' $CONFIG - fi - - if grep -q "list domain_list 'ru_outside'" $CONFIG; then - log "Deprecated list found: sru_outside" - sed -i '/ru_outside/d' $CONFIG - fi - - if grep -q "list domain_list 'ua'" $CONFIG; then - log "Deprecated list found: ua" - sed -i '/ua/d' $CONFIG - fi - - # Subnet list - if grep -q "list subnets" $CONFIG; then - log "Deprecated second section found" - sed -i '/list subnets/d' $CONFIG - fi - - # second remove - if grep -q "config second 'second'" $CONFIG; then - log "Deprecated second section found" - sed -i '/second/d' $CONFIG - fi - - # cron update - if grep -qE "^\s*option update_interval '[0-9*/,-]+( [0-9*/,-]+){4}'" $CONFIG; then - log "Deprecated update_interval" - sed -i "s|^\(\s*option update_interval\) '[0-9*/,-]\+\( [0-9*/,-]\+\)\{4\}'|\1 '1d'|" $CONFIG - fi - - # dnsmasq https - if grep -q "^filter-rr=HTTPS" "/etc/dnsmasq.conf"; then - log "Found and removed filter-rr=HTTPS in dnsmasq config" - sed -i '/^filter-rr=HTTPS/d' "/etc/dnsmasq.conf" - fi - - # dhcp use-application-dns.net - if grep -q "use-application-dns.net" "/etc/config/dhcp"; then - log "Found and removed use-application-dns.net in dhcp config" - sed -i '/use-application-dns/d' "/etc/config/dhcp" - fi - - # corntab init.d - (crontab -l | grep -v "/etc/init.d/podkop list_update") | crontab - - - migration_rename_config_key "$CONFIG" "option" "domain_list_enabled" "community_lists_enabled" - migration_rename_config_key "$CONFIG" "list" "domain_list" "community_lists" - - migration_rename_config_key "$CONFIG" "option" "custom_domains_list_type" "user_domain_list_type" - migration_rename_config_key "$CONFIG" "option" "custom_domains_text" "user_domains_text" - migration_rename_config_key "$CONFIG" "list" "custom_domains" "user_domains" - - migration_rename_config_key "$CONFIG" "option" "custom_subnets_list_enabled" "user_subnet_list_type" - migration_rename_config_key "$CONFIG" "option" "custom_subnets_text" "user_subnets_text" - migration_rename_config_key "$CONFIG" "list" "custom_subnets" "user_subnets" - - migration_rename_config_key "$CONFIG" "option" "custom_local_domains_list_enabled" "local_domain_lists_enabled" - migration_rename_config_key "$CONFIG" "list" "custom_local_domains" "local_domain_lists" - - migration_rename_config_key "$CONFIG" "option" "custom_download_domains_list_enabled" "remote_domain_lists_enabled" - migration_rename_config_key "$CONFIG" "list" "custom_download_domains" "remote_domain_lists" - - migration_rename_config_key "$CONFIG" "option" "custom_download_subnets_list_enabled" "remote_subnet_lists_enabled" - migration_rename_config_key "$CONFIG" "list" "custom_download_subnets" "remote_subnet_lists" - - migration_rename_config_key "$CONFIG" "option" "cache_file" "cache_path" - migration_add_new_option "podkop" "main" "config_path" "/etc/sing-box/config.json" && config_load "$PODKOP_CONFIG" + : } validate_service() { @@ -283,9 +230,10 @@ validate_service() { } process_validate_service() { - local community_lists_enabled - config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0 - if [ "$community_lists_enabled" -eq 1 ]; then + local section="$1" + local community_lists + config_get community_lists "$section" "community_lists" + if [ -n "$community_lists" ]; then config_list_foreach "$section" "community_lists" validate_service fi } @@ -306,27 +254,27 @@ route_table_rule_mark() { grep -q "105 $table" /etc/iproute2/rt_tables || echo "105 $table" >> /etc/iproute2/rt_tables if ! ip route list table $table | grep -q "local default dev lo scope host"; then - log "Added route for tproxy" + log "Added route for tproxy" "debug" ip route add local 0.0.0.0/0 dev lo table $table else - log "Route for tproxy exists" + log "Route for tproxy exists" "debug" fi if ! ip rule list | grep -q "from all fwmark 0x105 lookup $table"; then - log "Create marking rule" + log "Create marking rule" "debug" ip -4 rule add fwmark 0x105 table $table priority 105 else - log "Marking rule exist" + log "Marking rule exist" "debug" fi } nft_init_interfaces_set() { nft_create_ifname_set "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" - local interface_list - config_get interface_list "main" "iface" "br-lan" + local source_network_interfaces + config_get source_network_interfaces "settings" "source_network_interfaces" "br-lan" - for interface in $interface_list; do + for interface in $source_network_interfaces; do nft add element inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" "{ $interface }" done } @@ -394,7 +342,7 @@ backup_dnsmasq_config_option() { dnsmasq_add_resolver() { local shutdown_correctly - config_get shutdown_correctly "main" "shutdown_correctly" + config_get shutdown_correctly "settings" "shutdown_correctly" if [ "$shutdown_correctly" -eq 0 ]; then log "Previous shutdown of podkop was not correct, reconfiguration of dnsmasq is not required" return 0 @@ -426,7 +374,7 @@ dnsmasq_add_resolver() { dnsmasq_restore() { log "Restoring the dnsmasq configuration" local shutdown_correctly - config_get shutdown_correctly "main" "shutdown_correctly" + config_get shutdown_correctly "settings" "shutdown_correctly" if [ "$shutdown_correctly" -eq 1 ]; then log "Previous shutdown of podkop was correct, reconfiguration of dnsmasq is not required" return 0 @@ -476,11 +424,11 @@ dnsmasq_restore() { add_cron_job() { ## Future: make a check so that it doesn't recreate many times - local community_lists_enabled remote_domain_lists_enabled remote_subnet_lists_enabled update_interval - config_get community_lists_enabled "$section" "community_lists_enabled" - config_get remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" - config_get remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" - config_get update_interval "main" "update_interval" + local community_lists remote_domain_lists remote_subnet_lists update_interval + config_get community_lists "$section" "community_lists" + config_get remote_domain_lists "$section" "remote_domain_lists" + config_get remote_subnet_lists "$section" "remote_subnet_lists" + config_get update_interval "settings" "update_interval" case "$update_interval" in "1h") @@ -504,9 +452,9 @@ add_cron_job() { ;; esac - if [ "$community_lists_enabled" -eq 1 ] || - [ "$remote_domain_lists_enabled" -eq 1 ] || - [ "$remote_subnet_lists_enabled" -eq 1 ]; then + if [ -n "$community_lists" ] || + [ -n "$remote_domain_lists" ] || + [ -n "$remote_subnet_lists" ]; then remove_cron_job crontab -l | { cat @@ -541,8 +489,8 @@ list_update() { fi for i in $(seq 1 60); do - config_get_bool detour "main" "detour" "0" - if [ "$detour" -eq 1 ]; then + config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" "0" + if [ "$download_lists_via_proxy" -eq 1 ]; then if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com > /dev/null; then echolog "✅ GitHub connection check passed (via proxy)" break @@ -565,9 +513,9 @@ list_update() { echolog "📥 Downloading and processing lists..." - config_foreach import_community_subnet_lists - config_foreach import_domains_from_remote_domain_lists - config_foreach import_subnets_from_remote_subnet_lists + config_foreach import_community_subnet_lists "section" + config_foreach import_domains_from_remote_domain_lists "section" + config_foreach import_subnets_from_remote_subnet_lists "section" if [ $? -eq 0 ]; then echolog "✅ Lists update completed successfully" @@ -594,7 +542,7 @@ sing_box_uci() { log "sing-box service user has been changed to root" fi - config_get sing_box_config_path "main" "config_path" + config_get sing_box_config_path "settings" "config_path" sing_box_conffile=$(uci get "sing-box.main.conffile") log "sing-box config path: $sing_box_config_path" "debug" log "sing-box service conffile: $sing_box_conffile" "debug" @@ -643,17 +591,17 @@ sing_box_configure_outbounds() { config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG") - config_foreach configure_outbound_handler + config_foreach configure_outbound_handler "section" } configure_outbound_handler() { local section="$1" - local connection_mode - config_get connection_mode "$section" "mode" - case "$connection_mode" in + local connection_type + config_get connection_type "$section" "connection_type" + case "$connection_type" in proxy) - log "Configuring outbound in proxy connection mode for the $section section" + log "Configuring outbound in proxy connection type for the $section section" local proxy_config_type config_get proxy_config_type "$section" "proxy_config_type" @@ -662,15 +610,13 @@ configure_outbound_handler() { log "Detected proxy configuration type: url" "debug" local proxy_string udp_over_tcp config_get proxy_string "$section" "proxy_string" - config_get udp_over_tcp "$section" "ss_uot" - # Extract the first non-comment line as the active configuration - active_proxy_string=$(echo "$proxy_string" | grep -v "^[[:space:]]*\/\/" | head -n 1) + config_get udp_over_tcp "$section" "enable_udp_over_tcp" - if [ -z "$active_proxy_string" ]; then + if [ -z "$proxy_string" ]; then log "Proxy string is not set. Aborted." "fatal" exit 1 fi - config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$active_proxy_string" "$udp_over_tcp") + config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$proxy_string" "$udp_over_tcp") ;; outbound) log "Detected proxy configuration type: outbound" "debug" @@ -683,7 +629,7 @@ configure_outbound_handler() { local urltest_proxy_links udp_over_tcp i urltest_tag selector_tag outbound_tag outbound_tags \ urltest_outbounds selector_outbounds config_get urltest_proxy_links "$section" "urltest_proxy_links" - config_get udp_over_tcp "$section" "ss_uot" + config_get udp_over_tcp "$section" "enable_udp_over_tcp" if [ -z "$urltest_proxy_links" ]; then log "URLTest proxy links is not set. Aborted." "fatal" @@ -716,7 +662,7 @@ configure_outbound_handler() { esac ;; vpn) - log "Configuring outbound in VPN connection mode for the $section section" + log "Configuring outbound in VPN connection type for the $section section" local interface_name domain_resolver_enabled domain_resolver_dns_type domain_resolver_dns_server \ domain_resolver_dns_server_address outbound_tag domain_resolver_tag dns_domain_resolver @@ -746,10 +692,10 @@ configure_outbound_handler() { config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag") ;; block) - log "Connection mode 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" + log "Connection type 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" ;; *) - log "Unknown connection mode '$connection_mode' for the $section section. Aborted." "fatal" + log "Unknown connection type '$connection_type' for the $section section. Aborted." "fatal" exit 1 ;; esac @@ -761,9 +707,9 @@ sing_box_configure_dns() { log "Adding DNS Servers" "debug" local dns_type dns_server bootstrap_dns_server dns_domain_resolver dns_server_address - config_get dns_type "main" "dns_type" "doh" - config_get dns_server "main" "dns_server" "1.1.1.1" - config_get bootstrap_dns_server "main" "bootstrap_dns_server" "77.88.8.8" + config_get dns_type "settings" "dns_type" "doh" + config_get dns_server "settings" "dns_server" "1.1.1.1" + config_get bootstrap_dns_server "settings" "bootstrap_dns_server" "77.88.8.8" dns_server_address="$(url_get_host "$dns_server")" if ! is_ipv4 "$dns_server_address"; then @@ -776,7 +722,7 @@ sing_box_configure_dns() { log "Adding DNS Rules" local rewrite_ttl service_domains - config_get rewrite_ttl "main" "dns_rewrite_ttl" "60" + config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60" config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS") config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"') @@ -789,57 +735,60 @@ sing_box_configure_dns() { sing_box_configure_route() { log "Configure the route section of a sing-box JSON configuration" - config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG") - - local sniff_inbounds mixed_inbound_enabled - config_get_bool mixed_inbound_enabled "main" "socks5" 0 - if [ "$mixed_inbound_enabled" -eq 1 ]; then - sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG,$SB_MIXED_INBOUND_TAG") + local output_network_interface + config_get output_network_interface "settings" "output_network_interface" + if [ -z "$output_network_interface" ]; then + config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG") else - sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG") + config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" false "$SB_DNS_SERVER_TAG" \ + "$output_network_interface") fi + + local sniff_inbounds + sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG") config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds") config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns") - local quic_disable - config_get_bool quic_disable "main" "quic_disable" 0 - if [ "$quic_disable" -eq 1 ]; then + local disable_quic + config_get_bool disable_quic "settings" "disable_quic" 0 + if [ "$disable_quic" -eq 1 ]; then config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "protocol" "quic") fi - config=$( - sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$SB_MAIN_OUTBOUND_TAG" - ) + local first_outbound_section + first_outbound_section="$(get_first_outbound_section)" + first_outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")" + config=$(sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$first_outbound_tag") config=$(sing_box_cf_override_domain_port "$config" "$FAKEIP_TEST_DOMAIN" 8443) - config_foreach include_source_ips_in_routing_handler + config_foreach include_source_ips_in_routing_handler "section" configure_common_reject_route_rule - local exclude_from_ip_enabled - config_get_bool exclude_from_ip_enabled "main" "exclude_from_ip_enabled" 0 - if [ "$exclude_from_ip_enabled" -eq 1 ]; then + local routing_excluded_ips + config_get_bool routing_excluded_ips "settings" "routing_excluded_ips" + if [ -n "$routing_excluded_ips" ]; then rule_tag="$(gen_id)" config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG") - config_list_foreach "main" "exclude_traffic_ip" exclude_source_ip_from_routing_handler "$rule_tag" + config_list_foreach "settings" "routing_excluded_ips" exclude_source_ip_from_routing_handler "$rule_tag" fi - config_foreach configure_routing_for_section_lists + config_foreach configure_routing_for_section_lists "section" } include_source_ips_in_routing_handler() { local section="$1" - local all_traffic_from_ip_enabled rule_tag - config_get all_traffic_from_ip_enabled "$section" "all_traffic_from_ip_enabled" 0 - if [ "$all_traffic_from_ip_enabled" -eq 1 ]; then + local fully_routed_ips rule_tag + config_get fully_routed_ips "$section" "fully_routed_ips" + if [ -n "$fully_routed_ips" ]; then rule_tag="$(gen_id)" config=$( sing_box_cm_add_route_rule \ "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$(get_outbound_tag_by_section "$section")" ) - config_list_foreach "$section" "all_traffic_ip" include_source_ip_in_routing_handler "$rule_tag" + config_list_foreach "$section" "fully_routed_ips" include_source_ip_in_routing_handler "$rule_tag" fi } @@ -880,23 +829,24 @@ exclude_source_ip_from_routing_handler() { configure_routing_for_section_lists() { local section="$1" + log "Configuring routing for '$section' section" if ! section_has_enabled_lists "$section"; then log "Section '$section' does not have any enabled list, skipping..." "warn" return 0 fi - local community_lists_enabled user_domain_list_type local_domain_lists_enabled remote_domain_lists_enabled \ - user_subnet_list_type local_subnet_lists_enabled remote_subnet_lists_enabled section_mode_type route_rule_tag - config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0 + local community_lists user_domain_list_type user_subnet_list_type local_domain_lists local_subnet_lists \ + remote_domain_lists remote_subnet_lists section_connection_type route_rule_tag + config_get community_lists "$section" "community_lists" config_get user_domain_list_type "$section" "user_domain_list_type" "disabled" - config_get_bool local_domain_lists_enabled "$section" "local_domain_lists_enabled" 0 - config_get_bool remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" 0 config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled" - config_get_bool local_subnet_lists_enabled "$section" "local_subnet_lists_enabled" 0 - config_get_bool remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" 0 - config_get section_mode_type "$section" "mode" + config_get local_domain_lists "$section" "local_domain_lists" + config_get local_subnet_lists "$section" "local_subnet_lists" + config_get remote_domain_lists "$section" "remote_domain_lists" + config_get remote_subnet_lists "$section" "remote_subnet_lists" + config_get section_connection_type "$section" "connection_type" - if [ "$section_mode_type" = "block" ]; then + if [ "$section_connection_type" = "block" ]; then route_rule_tag="$SB_REJECT_RULE_TAG" else route_rule_tag="$(gen_id)" @@ -904,7 +854,7 @@ configure_routing_for_section_lists() { config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") fi - if [ "$community_lists_enabled" -eq 1 ]; then + if [ -n "$community_lists" ]; then log "Processing community list routing rules for '$section' section" config_list_foreach "$section" "community_lists" configure_community_list_handler "$section" "$route_rule_tag" fi @@ -915,30 +865,30 @@ configure_routing_for_section_lists() { configure_user_domain_or_subnets_list "$section" "domains" "$route_rule_tag" fi - if [ "$local_domain_lists_enabled" -eq 1 ]; then - log "Processing local domains routing rules for '$section' section" - configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag" - fi - - if [ "$remote_domain_lists_enabled" -eq 1 ]; then - log "Processing remote domains routing rules for '$section' section" - prepare_common_ruleset "$section" "domains" "$route_rule_tag" - config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \ - "domains" "$section" "$route_rule_tag" - fi - if [ "$user_subnet_list_type" != "disabled" ]; then log "Processing user subnets routing rules for '$section' section" prepare_common_ruleset "$section" "subnets" "$route_rule_tag" configure_user_domain_or_subnets_list "$section" "subnets" "$route_rule_tag" fi - if [ "$local_subnet_lists_enabled" -eq 1 ]; then + if [ -n "$local_domain_lists" ]; then + log "Processing local domains routing rules for '$section' section" + configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag" + fi + + if [ -n "$local_subnet_lists" ]; then log "Processing local subnets routing rules for '$section' section" configure_local_domain_or_subnet_lists "$section" "subnets" "$route_rule_tag" fi - if [ "$remote_subnet_lists_enabled" -eq 1 ]; then + if [ -n "$remote_domain_lists" ]; then + log "Processing remote domains routing rules for '$section' section" + prepare_common_ruleset "$section" "domains" "$route_rule_tag" + config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \ + "domains" "$section" "$route_rule_tag" + fi + + if [ -n "$remote_subnet_lists" ]; then log "Processing remote subnets routing rules for '$section' section" prepare_common_ruleset "$section" "subnets" "$route_rule_tag" config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \ @@ -966,7 +916,7 @@ prepare_common_ruleset() { config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag") ;; subnets) ;; - *) log "Unsupported remote rule set type: $type" "warn" ;; + *) log "Unsupported remote rule set type: $type" "error" ;; esac fi } @@ -981,7 +931,7 @@ configure_community_list_handler() { format="binary" url="$SRS_MAIN_URL/$tag.srs" detour="$(get_download_detour_tag)" - config_get update_interval "main" "update_interval" "1d" + config_get update_interval "settings" "update_interval" "1d" config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval") config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag") @@ -1050,7 +1000,7 @@ configure_local_domain_or_subnet_lists() { config_list_foreach "$section" "local_subnet_lists" import_local_domain_or_subnet_list "$type" \ "$section" "$ruleset_filepath" ;; - *) log "Unsupported local rule set type: $type" "warn" ;; + *) log "Unsupported local rule set type: $type" "error" ;; esac } @@ -1061,7 +1011,7 @@ import_local_domain_or_subnet_list() { local ruleset_filepath="$4" if ! file_exists "$filepath"; then - log "File $filepath not found" "warn" + log "File $filepath not found" "error" return 1 fi @@ -1069,7 +1019,7 @@ import_local_domain_or_subnet_list() { items="$(parse_domain_or_subnet_file_to_comma_string "$filepath" "$type")" if [ -z "$items" ]; then - log "No valid $type found in $filepath" + log "No valid $type found in $filepath" "warn" return 0 fi @@ -1099,7 +1049,7 @@ configure_remote_domain_or_subnet_list_handler() { ruleset_tag=$(get_ruleset_tag "$section" "$basename" "remote-$type") format="$(get_ruleset_format_by_file_extension "$file_extension")" detour="$(get_download_detour_tag)" - config_get update_interval "main" "update_interval" "1d" + config_get update_interval "settings" "update_interval" "1d" config=$(sing_box_cm_add_remote_ruleset "$config" "$ruleset_tag" "$format" "$url" "$detour" "$update_interval") config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag") @@ -1108,7 +1058,7 @@ configure_remote_domain_or_subnet_list_handler() { config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag") ;; subnets) ;; - *) log "Unsupported remote rule set type: $type" "warn" ;; + *) log "Unsupported remote rule set type: $type" "error" ;; esac ;; *) @@ -1122,13 +1072,13 @@ sing_box_configure_experimental() { log "Configuring cache database" local cache_file - config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db" + config_get cache_file "settings" "cache_path" "/tmp/sing-box/cache.db" config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true) - local yacd_enabled external_controller_ui - config_get_bool yacd_enabled "main" "yacd" 0 + local enable_yacd external_controller_ui + config_get_bool enable_yacd "settings" "enable_yacd" 0 log "Configuring Clash API" - if [ "$yacd_enabled" -eq 1 ]; then + if [ "$enable_yacd" -eq 1 ]; then log "YACD is enabled, enabling Clash API with downloadable YACD" "debug" local external_controller_ui="ui" config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER" "$external_controller_ui") @@ -1141,32 +1091,48 @@ sing_box_configure_experimental() { sing_box_additional_inbounds() { log "Configure the additional inbounds of a sing-box JSON configuration" - local mixed_inbound_enabled - config_get_bool mixed_inbound_enabled "main" "socks5" 0 - if [ "$mixed_inbound_enabled" -eq 1 ]; then + local download_lists_via_proxy + config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0 + if [ "$download_lists_via_proxy" -eq 1 ]; then + local download_lists_via_proxy_section section_outbound_tag + config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section" + section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")" config=$( sing_box_cf_add_mixed_inbound_and_route_rule \ "$config" \ - "$SB_MIXED_INBOUND_TAG" \ - "$SB_MIXED_INBOUND_ADDRESS" \ - "$SB_MIXED_INBOUND_PORT" \ - "$SB_MAIN_OUTBOUND_TAG" + "$SB_SERVICE_MIXED_INBOUND_TAG" \ + "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \ + "$SB_SERVICE_MIXED_INBOUND_PORT" \ + "$section_outbound_tag" ) fi - config=$( - sing_box_cf_add_mixed_inbound_and_route_rule \ - "$config" \ - "$SB_SERVICE_MIXED_INBOUND_TAG" \ - "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \ - "$SB_SERVICE_MIXED_INBOUND_PORT" \ - "$SB_MAIN_OUTBOUND_TAG" - ) + config_foreach configure_section_mixed_proxy "section" +} + +configure_section_mixed_proxy() { + local section="$1" + + local mixed_inbound_enabled mixed_proxy_port mixed_inbound_tag mixed_outbound_tag + config_get_bool mixed_inbound_enabled "$section" "mixed_proxy_enabled" 0 + config_get mixed_proxy_port "$section" "mixed_proxy_port" + if [ "$mixed_inbound_enabled" -eq 1 ]; then + mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")" + mixed_outbound_tag="$(get_outbound_tag_by_section "$section")" + config=$( + sing_box_cf_add_mixed_inbound_and_route_rule \ + "$config" \ + "$mixed_inbound_tag" \ + "$SB_MIXED_INBOUND_ADDRESS" \ + "$mixed_proxy_port" \ + "$mixed_outbound_tag" + ) + fi } sing_box_save_config() { local sing_box_config_path temp_file_path current_config_hash temp_config_hash - config_get sing_box_config_path "main" "config_path" + config_get sing_box_config_path "settings" "config_path" temp_file_path="$(mktemp)" log "Save sing-box temporary config to $temp_file_path" "debug" @@ -1191,16 +1157,16 @@ sing_box_config_check() { local config_path="$1" if ! sing-box -c "$config_path" check > /dev/null 2>&1; then - log "Sing-box configuration $config_path is invalid" "fatal" + log "Sing-box configuration $config_path is invalid. Aborted." "fatal" exit 1 fi } import_community_subnet_lists() { local section="$1" - local community_lists_enabled - config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0 - if [ "$community_lists_enabled" -eq 1 ]; then + local community_lists + config_get community_lists "$section" "community_lists" + if [ -n "$community_lists" ]; then log "Importing community subnet lists for '$section' section" config_list_foreach "$section" "community_lists" import_community_service_subnet_list_handler fi @@ -1243,7 +1209,7 @@ import_community_service_subnet_list_handler() { *) return 0 ;; esac - local tmpfile detour http_proxy_address subnets + local tmpfile http_proxy_address subnets tmpfile=$(mktemp) http_proxy_address="$(get_service_proxy_address)" @@ -1266,9 +1232,9 @@ import_community_service_subnet_list_handler() { import_domains_from_remote_domain_lists() { local section="$1" - local remote_domain_lists_enabled - config_get remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" - if [ "$remote_domain_lists_enabled" -eq 1 ]; then + local remote_domain_lists + config_get remote_domain_lists "$section" "remote_domain_lists" + if [ -n "$remote_domain_lists" ]; then log "Importing domains from remote domain lists for '$section' section" config_list_foreach "$section" "remote_domain_lists" import_domains_from_remote_domain_list_handler "$section" fi @@ -1295,9 +1261,9 @@ import_domains_from_remote_domain_list_handler() { import_subnets_from_remote_subnet_lists() { local section="$1" - - config_get remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" - if [ "$remote_subnet_lists_enabled" -eq 1 ]; then + local remote_subnet_lists + config_get remote_subnet_lists "$section" "remote_subnet_lists" + if [ -n "$remote_subnet_lists" ]; then log "Importing subnets from remote subnet lists for '$section' section" config_list_foreach "$section" "remote_subnet_lists" import_subnets_from_remote_subnet_list_handler "$section" fi @@ -1347,7 +1313,7 @@ import_domains_or_subnets_from_remote_file() { rm -f "$tmpfile" if [ -z "$items" ]; then - log "No valid $type found in $url" + log "No valid $type found in $url" "warn" return 0 fi @@ -1411,9 +1377,9 @@ import_subnets_from_remote_srs_file() { ## Support functions get_service_proxy_address() { - local detour - config_get_bool detour "main" "detour" 0 - if [ "$detour" -eq 1 ]; then + local download_lists_via_proxy + config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0 + if [ "$download_lists_via_proxy" -eq 1 ]; then echo "$SB_SERVICE_MIXED_INBOUND_ADDRESS:$SB_SERVICE_MIXED_INBOUND_PORT" else echo "" @@ -1421,20 +1387,42 @@ get_service_proxy_address() { } get_download_detour_tag() { - config_get_bool detour "main" "detour" 0 - if [ "${detour:-0}" -eq 1 ]; then - echo "$SB_MAIN_OUTBOUND_TAG" + config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0 + if [ "$download_lists_via_proxy" -eq 1 ]; then + local download_lists_via_proxy_section section_outbound_tag + config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section" + section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")" + echo "$section_outbound_tag" else echo "" fi } +_determine_first_outbound_section() { + local section="$1" + + local connection_type + config_get connection_type "$section" "connection_type" + + if [ "$connection_type" = "proxy" ] || [ "$connection_type" = "vpn" ]; then + [ -z "$first_section" ] && first_section="$1" + fi +} + +get_first_outbound_section() { + local first_section="" + + config_foreach _determine_first_outbound_section "section" + + echo "$first_section" +} + get_block_sections() { - uci show podkop | grep "\.mode='block'" | cut -d'.' -f2 + uci show podkop | grep "\.connection_type='block'" | cut -d'.' -f2 } block_section_exists() { - if uci show podkop | grep -q "\.mode='block'"; then + if uci show podkop | grep -q "\.connection_type='block'"; then return 0 else return 1 @@ -1443,24 +1431,24 @@ block_section_exists() { section_has_enabled_lists() { local section="$1" - local community_lists_enabled user_domain_list_type local_domain_lists_enabled remote_domain_lists_enabled \ - user_subnet_list_type local_subnet_lists_enabled remote_subnet_lists_enabled + local community_lists user_domain_list_type user_subnet_list_type local_domain_lists local_subnet_lists \ + remote_domain_lists remote_subnet_lists - config_get_bool community_lists_enabled "$section" "community_lists_enabled" 0 + config_get community_lists "$section" "community_lists" config_get user_domain_list_type "$section" "user_domain_list_type" "disabled" - config_get_bool local_domain_lists_enabled "$section" "local_domain_lists_enabled" 0 - config_get_bool remote_domain_lists_enabled "$section" "remote_domain_lists_enabled" 0 config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled" - config_get_bool local_subnet_lists_enabled "$section" "local_subnet_lists_enabled" 0 - config_get_bool remote_subnet_lists_enabled "$section" "remote_subnet_lists_enabled" 0 + config_get local_domain_lists "$section" "local_domain_lists" + config_get local_subnet_lists "$section" "local_subnet_lists" + config_get remote_domain_lists "$section" "remote_domain_lists" + config_get remote_subnet_lists "$section" "remote_subnet_lists" - if [ "$community_lists_enabled" -ne 0 ] || + if [ -n "$community_lists" ] || [ "$user_domain_list_type" != "disabled" ] || - [ "$local_domain_lists_enabled" -ne 0 ] || - [ "$remote_domain_lists_enabled" -ne 0 ] || [ "$user_subnet_list_type" != "disabled" ] || - [ "$local_subnet_lists_enabled" -ne 0 ] || - [ "$remote_subnet_lists_enabled" -ne 0 ]; then + [ -n "$local_domain_lists" ] || + [ -n "$local_subnet_lists" ] || + [ -n "$remote_domain_lists" ] || + [ -n "$remote_subnet_lists" ]; then return 0 else return 1 @@ -1481,7 +1469,7 @@ nft_list_all_traffic_from_ip() { # Diagnotics check_proxy() { local sing_box_config_path - config_get sing_box_config_path "main" "config_path" + config_get sing_box_config_path "settings" "config_path" if ! command -v sing-box > /dev/null 2>&1; then nolog "sing-box is not installed" @@ -1625,106 +1613,35 @@ check_nft() { nolog "NFT check completed" } -check_github() { - nolog "Checking GitHub connectivity..." - - if ! curl -m 3 github.com; then - nolog "Error: Cannot connect to GitHub" - return 1 - fi - nolog "GitHub is accessible" - - nolog "Checking lists availability:" - for url in "$DOMAINS_RU_INSIDE" "$DOMAINS_RU_OUTSIDE" "$DOMAINS_UA" "$DOMAINS_YOUTUBE" \ - "$SUBNETS_TWITTER" "$SUBNETS_META" "$SUBNETS_DISCORD"; do - local list_name=$(basename "$url") - - config_get_bool detour "main" "detour" "0" - if [ "$detour" -eq 1 ]; then - http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" wget -q -O /dev/null "$url" - else - wget -q -O /dev/null "$url" - fi - - if [ $? -eq 0 ]; then - nolog "- $list_name: available" - else - nolog "- $list_name: not available" - fi - done -} - -check_dnsmasq() { - nolog "Checking dnsmasq configuration..." - - local config=$(uci show dhcp.@dnsmasq[0]) - if [ -z "$config" ]; then - nolog "No dnsmasq configuration found" - return 1 - fi - - echo "$config" | while IFS='=' read -r key value; do - nolog "$key = $value" - done -} - -check_sing_box_connections() { - nolog "Checking sing-box connections..." - - if ! command -v netstat > /dev/null 2>&1; then - nolog "netstat is not installed" - return 1 - fi - - local connections=$(netstat -tuanp | grep sing-box) - if [ -z "$connections" ]; then - nolog "No active sing-box connections found" - return 1 - fi - - echo "$connections" | while read -r line; do - nolog "$line" - done -} - -check_sing_box_logs() { - nolog "Showing sing-box logs from system journal..." - - local logs=$(logread -e sing-box | tail -n 50) - if [ -z "$logs" ]; then - nolog "No sing-box logs found" - return 1 - fi - - echo "$logs" -} - check_logs() { - nolog "Showing podkop logs from system journal..." - if ! command -v logread > /dev/null 2>&1; then nolog "Error: logread command not found" return 1 fi - # Get all logs first - local all_logs=$(logread) + local logs + logs=$(logread | grep -E "podkop|sing-box") - # Find the last occurrence of "Starting podkop" - local start_line=$(echo "$all_logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1) - - if [ -z "$start_line" ]; then - nolog "No 'Starting podkop' message found in logs" + if [ -z "$logs" ]; then + nolog "Logs not found" return 1 fi + ы + # Find the last occurrence of "Starting podkop" + local start_line + start_line=$(echo "$logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1) - # Output all logs from the last start - echo "$all_logs" | tail -n +"$start_line" + if [ -n "$start_line" ]; then + echo "$logs" | tail -n +"$start_line" + else + nolog "No 'Starting podkop' message found, showing last 100 lines" + echo "$logs" | tail -n 100 + fi } show_sing_box_config() { local sing_box_config_path - config_get sing_box_config_path "main" "config_path" + config_get sing_box_config_path "settings" "config_path" nolog "Current sing-box configuration:" if [ ! -f "$sing_box_config_path" ]; then @@ -1788,7 +1705,8 @@ show_version() { } show_sing_box_version() { - local version=$(sing-box version | head -n 1 | awk '{print $3}') + local version + version=$(sing-box version | head -n 1 | awk '{print $3}') echo "$version" } @@ -1800,6 +1718,44 @@ show_system_info() { cat /tmp/sysinfo/model } +get_system_info() { + local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model + + podkop_version="$PODKOP_VERSION" + + podkop_latest_version=$(curl -m 3 -s https://api.github.com/repos/itdoginfo/podkop/releases/latest | grep '"tag_name":' | cut -d'"' -f4) + [ -z "$podkop_latest_version" ] && podkop_latest_version="unknown" + + if [ -f /www/luci-static/resources/view/podkop/main.js ]; then + luci_app_version=$(grep 'var PODKOP_LUCI_APP_VERSION' /www/luci-static/resources/view/podkop/main.js | cut -d'"' -f2) + else + luci_app_version="not installed" + fi + + if command -v sing-box > /dev/null 2>&1; then + sing_box_version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $3}') + [ -z "$sing_box_version" ] && sing_box_version="unknown" + else + sing_box_version="not installed" + fi + + if [ -f /etc/os-release ]; then + openwrt_version=$(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2) + [ -z "$openwrt_version" ] && openwrt_version="unknown" + else + openwrt_version="unknown" + fi + + if [ -f /tmp/sysinfo/model ]; then + device_model=$(cat /tmp/sysinfo/model) + [ -z "$device_model" ] && device_model="unknown" + else + device_model="unknown" + fi + + echo "{\"podkop_version\": \"$podkop_version\", \"podkop_latest_version\": \"$podkop_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq . +} + get_sing_box_status() { local running=0 local enabled=0 @@ -1819,7 +1775,8 @@ get_sing_box_status() { fi # Check DNS configuration - local dns_server=$(uci get dhcp.@dnsmasq[0].server 2> /dev/null) + local dns_server + dns_server=$(uci get dhcp.@dnsmasq[0].server 2> /dev/null) if [ "$dns_server" = "127.0.0.42" ]; then dns_configured=1 fi @@ -1858,91 +1815,353 @@ get_status() { } check_dns_available() { - local dns_type=$(uci get podkop.main.dns_type 2> /dev/null) - local dns_server=$(uci get podkop.main.dns_server 2> /dev/null) - local is_available=0 - local status="unavailable" - local local_dns_working=0 - local local_dns_status="unavailable" + local dns_type dns_server bootstrap_dns_server + config_get dns_type "settings" "dns_type" + config_get dns_server "settings" "dns_server" + config_get bootstrap_dns_server "settings" "bootstrap_dns_server" + + local dns_status=0 + local dns_on_router=0 + local bootstrap_dns_status=0 + local dhcp_config_status=1 + local domain="google.com" # Mask NextDNS ID if present local display_dns_server="$dns_server" if echo "$dns_server" | grep -q "\.dns\.nextdns\.io$"; then - local nextdns_id=$(echo "$dns_server" | cut -d'.' -f1) + local nextdns_id + nextdns_id=$(echo "$dns_server" | cut -d'.' -f1) display_dns_server="$(echo "$nextdns_id" | sed 's/./*/g').dns.nextdns.io" elif echo "$dns_server" | grep -q "^dns\.nextdns\.io/"; then - local masked_path=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g') + local masked_path + masked_path=$(echo "$dns_server" | cut -d'/' -f2- | sed 's/./*/g') display_dns_server="dns.nextdns.io/$masked_path" fi if [ "$dns_type" = "doh" ]; then - # Generate random DNS query ID (2 bytes) - local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"' 2> /dev/null) - if [ $? -ne 0 ]; then - error_message="Failed to generate random ID" - status="internal error" - else - # Create DNS wire format query for google.com A record with random ID - local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64 2> /dev/null) - if [ $? -ne 0 ]; then - error_message="Failed to generate DNS query" - status="internal error" - else - # Try POST method first (RFC 8484 compliant) with shorter timeout - local result=$(echo "$dns_query" | base64 -d 2> /dev/null | curl -H "Content-Type: application/dns-message" \ - -H "Accept: application/dns-message" \ - --data-binary @- \ - --max-time 2 \ - --connect-timeout 1 \ - -s \ - "https://$dns_server/dns-query" 2> /dev/null) + # Check if dns_server already contains a path + local doh_path="/dns-query" + if echo "$dns_server" | grep -q "/"; then + # Path is already present, extract it + doh_path="/$(echo "$dns_server" | cut -d'/' -f2-)" + dns_server="$(echo "$dns_server" | cut -d'/' -f1)" + fi - if [ $? -eq 0 ] && [ -n "$result" ]; then - is_available=1 - status="available" - else - # Try GET method as fallback with shorter timeout - local dns_query_no_padding=$(echo "$dns_query" | tr -d '=' 2> /dev/null) - result=$(curl -H "accept: application/dns-message" \ - --max-time 2 \ - --connect-timeout 1 \ - -s \ - "https://$dns_server/dns-query?dns=$dns_query_no_padding" 2> /dev/null) - - if [ $? -eq 0 ] && [ -n "$result" ]; then - is_available=1 - status="available" - else - error_message="DoH server not responding" - fi - fi - fi + if dig @"$dns_server" "$domain" +https="$doh_path" +timeout=2 +tries=1 > /dev/null 2>&1; then + dns_status=1 fi elif [ "$dns_type" = "dot" ]; then - (nc "$dns_server" 853 < /dev/null > /dev/null 2>&1) & - pid=$! - sleep 2 - if kill -0 $pid 2> /dev/null; then - kill $pid 2> /dev/null - wait $pid 2> /dev/null - else - is_available=1 - status="available" + if dig @"$dns_server" "$domain" +tls +timeout=2 +tries=1 > /dev/null 2>&1; then + dns_status=1 fi elif [ "$dns_type" = "udp" ]; then - if nslookup -timeout=2 itdog.info $dns_server > /dev/null 2>&1; then - is_available=1 - status="available" + if dig @"$dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then + dns_status=1 fi fi # Check if local DNS resolver is working - if nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.1 > /dev/null 2>&1; then - local_dns_working=1 - local_dns_status="available" + if dig @127.0.0.1 "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then + dns_on_router=1 fi - echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"is_available\":$is_available,\"status\":\"$status\",\"local_dns_working\":$local_dns_working,\"local_dns_status\":\"$local_dns_status\"}" + # Check bootstrap DNS server + if [ -n "$bootstrap_dns_server" ]; then + if dig @"$bootstrap_dns_server" "$domain" +timeout=2 +tries=1 > /dev/null 2>&1; then + bootstrap_dns_status=1 + fi + fi + + # Check if /etc/config/dhcp has server 127.0.0.42 + config_load dhcp + config_foreach check_dhcp_has_podkop_dns dnsmasq + config_load "$PODKOP_CONFIG" + + echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"dns_status\":$dns_status,\"dns_on_router\":$dns_on_router,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_config_status\":$dhcp_config_status}" | jq . +} + +check_dhcp_has_podkop_dns() { + local server_list cachesize noresolv server_found + config_get server_list "$1" "server" + config_get cachesize "$1" "cachesize" + config_get noresolv "$1" "noresolv" + + server_found=0 + + if [ -n "$server_list" ]; then + for server in $server_list; do + if [ "$server" = "127.0.0.42" ]; then + server_found=1 + break + fi + done + fi + + if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server_found" != "1" ]; then + dhcp_config_status=0 + fi +} + +check_nft_rules() { + local table_exist=0 + local rules_mangle_exist=0 + local rules_mangle_counters=0 + local rules_mangle_output_exist=0 + local rules_mangle_output_counters=0 + local rules_proxy_exist=0 + local rules_proxy_counters=0 + local rules_other_mark_exist=0 + + # Generate traffic through PodkopTable + curl -m 3 -s "https://$CHECK_PROXY_IP_DOMAIN/check" > /dev/null 2>&1 & + local pid1=$! + curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" > /dev/null 2>&1 & + local pid2=$! + + wait $pid1 2> /dev/null + wait $pid2 2> /dev/null + sleep 1 + + # Check if PodkopTable exists + if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then + table_exist=1 + + # Check mangle chain rules + if nft list chain inet "$NFT_TABLE_NAME" mangle > /dev/null 2>&1; then + local mangle_output + mangle_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle) + if echo "$mangle_output" | grep -q "counter"; then + rules_mangle_exist=1 + + if echo "$mangle_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then + rules_mangle_counters=1 + fi + fi + fi + + # Check mangle_output chain rules + if nft list chain inet "$NFT_TABLE_NAME" mangle_output > /dev/null 2>&1; then + local mangle_output_output + mangle_output_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle_output) + if echo "$mangle_output_output" | grep -q "counter"; then + rules_mangle_output_exist=1 + + if echo "$mangle_output_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then + rules_mangle_output_counters=1 + fi + fi + fi + + # Check proxy chain rules + if nft list chain inet "$NFT_TABLE_NAME" proxy > /dev/null 2>&1; then + local proxy_output + proxy_output=$(nft list chain inet "$NFT_TABLE_NAME" proxy) + if echo "$proxy_output" | grep -q "counter"; then + rules_proxy_exist=1 + + if echo "$proxy_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then + rules_proxy_counters=1 + fi + fi + fi + fi + + # Check for other mark rules outside PodkopTable + nft list tables 2> /dev/null | while read -r _ family table_name; do + [ -z "$table_name" ] && continue + + [ "$table_name" = "$NFT_TABLE_NAME" ] && continue + + if nft list table "$family" "$table_name" 2> /dev/null | grep -q "meta mark set"; then + touch /tmp/podkop_mark_check.$$ + break + fi + done + + if [ -f /tmp/podkop_mark_check.$$ ]; then + rules_other_mark_exist=1 + rm -f /tmp/podkop_mark_check.$$ + fi + + echo "{\"table_exist\":$table_exist,\"rules_mangle_exist\":$rules_mangle_exist,\"rules_mangle_counters\":$rules_mangle_counters,\"rules_mangle_output_exist\":$rules_mangle_output_exist,\"rules_mangle_output_counters\":$rules_mangle_output_counters,\"rules_proxy_exist\":$rules_proxy_exist,\"rules_proxy_counters\":$rules_proxy_counters,\"rules_other_mark_exist\":$rules_other_mark_exist}" | jq . +} + +check_sing_box() { + local sing_box_installed=0 + local sing_box_version_ok=0 + local sing_box_service_exist=0 + local sing_box_autostart_disabled=0 + local sing_box_process_running=0 + local sing_box_ports_listening=0 + + # Check if sing-box is installed + if command -v sing-box > /dev/null 2>&1; then + sing_box_installed=1 + + # Check version (must be >= 1.12.4) + local version + version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $3}') + if [ -n "$version" ]; then + version=$(echo "$version" | sed 's/^v//') + local major + local minor + local patch + major=$(echo "$version" | cut -d. -f1) + minor=$(echo "$version" | cut -d. -f2) + patch=$(echo "$version" | cut -d. -f3) + + # Compare version: must be >= 1.12.4 + if [ "$major" -gt 1 ] || + [ "$major" -eq 1 ] && [ "$minor" -gt 12 ] || + [ "$major" -eq 1 ] && [ "$minor" -eq 12 ] && [ "$patch" -ge 4 ]; then + sing_box_version_ok=1 + fi + fi + fi + + # Check if service exists + if [ -f /etc/init.d/sing-box ]; then + sing_box_service_exist=1 + + if ! /etc/init.d/sing-box enabled 2> /dev/null; then + sing_box_autostart_disabled=1 + fi + fi + + # Check if process is running + if pgrep "sing-box" > /dev/null 2>&1; then + sing_box_process_running=1 + fi + + # Check if sing-box is listening on required ports + local port_53_ok=0 + local port_1602_ok=0 + + if netstat -ln 2> /dev/null | grep -q "127.0.0.42:53"; then + port_53_ok=1 + fi + + if netstat -ln 2> /dev/null | grep -q "127.0.0.1:1602"; then + port_1602_ok=1 + fi + + # Both ports must be listening + if [ "$port_53_ok" = "1" ] && [ "$port_1602_ok" = "1" ]; then + sing_box_ports_listening=1 + fi + + echo "{\"sing_box_installed\":$sing_box_installed,\"sing_box_version_ok\":$sing_box_version_ok,\"sing_box_service_exist\":$sing_box_service_exist,\"sing_box_autostart_disabled\":$sing_box_autostart_disabled,\"sing_box_process_running\":$sing_box_process_running,\"sing_box_ports_listening\":$sing_box_ports_listening}" | jq . +} + +check_fakeip() { + curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" | jq . +} + +####################################### +# Clash API interface for managing proxies and groups +# Arguments: +# $1 - Action: get_proxies, get_proxy_latency, get_group_latency, set_group_proxy +# $2 - Proxy/Group tag (required for latency and set operations) +# $3 - Timeout in ms (optional, defaults: 2000 for proxy, 5000 for group) or target proxy tag for set_group_proxy +# Outputs: +# JSON formatted response +# Usage: +# clash_api get_proxies +# clash_api get_proxy_latency [timeout] +# clash_api get_group_latency [timeout] +# clash_api set_group_proxy +####################################### + +clash_api() { + local CLASH_URL="127.0.0.1:9090" + local TEST_URL="https://www.gstatic.com/generate_204" + local action="$1" + + case "$action" in + get_proxies) + curl -s "$CLASH_URL/proxies" | jq . + ;; + + get_proxy_latency) + local proxy_tag="$2" + local timeout="${3:-2000}" + + if [ -z "$proxy_tag" ]; then + echo '{"error":"proxy_tag required"}' | jq . + return 1 + fi + + curl -G -s "$CLASH_URL/proxies/$proxy_tag/delay" \ + --data-urlencode "url=$TEST_URL" \ + --data-urlencode "timeout=$timeout" | jq . + ;; + + get_group_latency) + local group_tag="$2" + local timeout="${3:-5000}" + + if [ -z "$group_tag" ]; then + echo '{"error":"group_tag required"}' | jq . + return 1 + fi + + curl -G -s "$CLASH_URL/group/$group_tag/delay" \ + --data-urlencode "url=$TEST_URL" \ + --data-urlencode "timeout=$timeout" | jq . + ;; + + set_group_proxy) + local group_tag="$2" + local proxy_tag="$3" + + if [ -z "$group_tag" ] || [ -z "$proxy_tag" ]; then + echo '{"error":"group_tag and proxy_tag required"}' | jq . + return 1 + fi + + local response + response=$(curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \ + --data-raw "{\"name\":\"$proxy_tag\"}") + + local http_code + local body + http_code=$(echo "$response" | tail -n 1) + body=$(echo "$response" | sed '$d') + + case "$http_code" in + 204) + echo "{\"success\":true,\"group\":\"$group_tag\",\"proxy\":\"$proxy_tag\"}" | jq . + ;; + 404) + echo "{\"success\":false,\"error\":\"group_not_found\",\"message\":\"$group_tag does not exist\"}" | jq . + return 1 + ;; + 400) + if echo "$body" | grep -q "not found"; then + echo "{\"success\":false,\"error\":\"proxy_not_found\",\"message\":\"$proxy_tag not found in group $group_tag\"}" | jq . + else + echo '{"success":false,"error":"bad_request","message":"Invalid request"}' | jq . + fi + return 1 + ;; + *) + if [ -n "$body" ]; then + local body_json + body_json=$(echo "$body" | jq -c .) + echo "{\"success\":false,\"http_code\":$http_code,\"body\":$body_json}" | jq . + else + echo "{\"success\":false,\"http_code\":$http_code}" | jq . + fi + return 1 + ;; + esac + ;; + + *) + echo '{"error":"unknown action","available":["get_proxies","get_proxy_latency","get_group_latency","set_group_proxy"]}' | jq . + return 1 + ;; + esac } print_global() { @@ -1950,16 +2169,6 @@ print_global() { echo "$message" } -find_working_resolver() { - for resolver in $DNS_RESOLVERS; do - if nslookup -timeout=2 "$FAKEIP_TEST_DOMAIN" "$resolver" > /dev/null 2>&1; then - echo "$resolver" - return 0 - fi - done - return 1 -} - global_check() { local PODKOP_LUCI_VERSION="Unknown" [ -n "$1" ] && PODKOP_LUCI_VERSION="$1" @@ -1967,50 +2176,229 @@ global_check() { print_global "📡 Global check run!" print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_global "🛠️ System info" - print_global "🕳️ Podkop: ${PODKOP_VERSION}" - print_global "🕳️ LuCI App: ${PODKOP_LUCI_VERSION}" - print_global "📦 Sing-box: $(sing-box version | head -n 1 | awk '{print $3}')" - print_global "🛜 OpenWrt: $(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2)" - print_global "🛜 Device: $(cat /tmp/sysinfo/model)" + + local system_info_json + system_info_json=$(get_system_info) + + if [ -n "$system_info_json" ]; then + local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model + + podkop_version=$(echo "$system_info_json" | jq -r '.podkop_version // "unknown"') + podkop_latest_version=$(echo "$system_info_json" | jq -r '.podkop_latest_version // "unknown"') + luci_app_version=$(echo "$system_info_json" | jq -r '.luci_app_version // "unknown"') + sing_box_version=$(echo "$system_info_json" | jq -r '.sing_box_version // "unknown"') + openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"') + device_model=$(echo "$system_info_json" | jq -r '.device_model // "unknown"') + + print_global "🕳️ Podkop: $podkop_version (latest: $podkop_latest_version)" + print_global "🕳️ LuCI App: $luci_app_version" + print_global "📦 Sing-box: $sing_box_version" + print_global "🛜 OpenWrt: $openwrt_version" + print_global "🛜 Device: $device_model" + else + print_global "❌ Failed to get system info" + fi + + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "➡️ DNS status" + + local dns_check_json + dns_check_json=$(check_dns_available) + + if [ -n "$dns_check_json" ]; then + local dns_type dns_server dns_status dns_on_router bootstrap_dns_server bootstrap_dns_status dhcp_config_status + + dns_type=$(echo "$dns_check_json" | jq -r '.dns_type // "unknown"') + dns_server=$(echo "$dns_check_json" | jq -r '.dns_server // "unknown"') + dns_status=$(echo "$dns_check_json" | jq -r '.dns_status // 0') + dns_on_router=$(echo "$dns_check_json" | jq -r '.dns_on_router // 0') + bootstrap_dns_server=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_server // ""') + bootstrap_dns_status=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_status // 0') + dhcp_config_status=$(echo "$dns_check_json" | jq -r '.dhcp_config_status // 0') + + # Bootstrap DNS + if [ -n "$bootstrap_dns_server" ]; then + if [ "$bootstrap_dns_status" -eq 1 ]; then + print_global "✅ Bootstrap DNS: $bootstrap_dns_server" + else + print_global "❌ Bootstrap DNS: $bootstrap_dns_server" + fi + fi + + # DNS server status + if [ "$dns_status" -eq 1 ]; then + print_global "✅ Main DNS: $dns_server [$dns_type]" + else + print_global "❌ Main DNS: $dns_server [$dns_type]" + fi + + # DNS on router + if [ "$dns_on_router" -eq 1 ]; then + print_global "✅ DNS on router" + else + print_global "❌ DNS on router" + fi + + # DHCP configuration check + local dont_touch_dhcp + config_get dont_touch_dhcp "settings" "dont_touch_dhcp" + + if [ "$dont_touch_dhcp" = "1" ]; then + print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:" + awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp + elif [ "$dhcp_config_status" -eq 0 ]; then + print_global "❌ DHCP configuration differs from template. 📄 DHCP config:" + awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp + else + print_global "✅ /etc/config/dhcp" + fi + else + print_global "❌ Failed to get DNS info" + fi + + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "📦 Sing-box status" + + local singbox_check_json + singbox_check_json=$(check_sing_box) + + if [ -n "$singbox_check_json" ]; then + local sing_box_installed sing_box_version_ok sing_box_service_exist sing_box_autostart_disabled sing_box_process_running sing_box_ports_listening + + sing_box_installed=$(echo "$singbox_check_json" | jq -r '.sing_box_installed // 0') + sing_box_version_ok=$(echo "$singbox_check_json" | jq -r '.sing_box_version_ok // 0') + sing_box_service_exist=$(echo "$singbox_check_json" | jq -r '.sing_box_service_exist // 0') + sing_box_autostart_disabled=$(echo "$singbox_check_json" | jq -r '.sing_box_autostart_disabled // 0') + sing_box_process_running=$(echo "$singbox_check_json" | jq -r '.sing_box_process_running // 0') + sing_box_ports_listening=$(echo "$singbox_check_json" | jq -r '.sing_box_ports_listening // 0') + + if [ "$sing_box_installed" -eq 1 ]; then + print_global "✅ Sing-box installed" + else + print_global "❌ Sing-box installed" + fi + + if [ "$sing_box_version_ok" -eq 1 ]; then + print_global "✅ Sing-box version >= 1.12.4" + else + print_global "❌ Sing-box version >= 1.12.4" + fi + + if [ "$sing_box_service_exist" -eq 1 ]; then + print_global "✅ Sing-box service exist" + else + print_global "❌ Sing-box service exist" + fi + + if [ "$sing_box_autostart_disabled" -eq 1 ]; then + print_global "✅ Sing-box autostart disabled" + else + print_global "❌ Sing-box autostart disabled" + fi + + if [ "$sing_box_process_running" -eq 1 ]; then + print_global "✅ Sing-box process running" + else + print_global "❌ Sing-box process running" + fi + + if [ "$sing_box_ports_listening" -eq 1 ]; then + print_global "✅ Sing-box listening ports" + else + print_global "❌ Sing-box listening ports" + fi + else + print_global "❌ Failed to get sing-box info" + fi + + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "🧱 NFT rules status" + + local nft_check_json + nft_check_json=$(check_nft_rules) + + if [ -n "$nft_check_json" ]; then + local table_exist rules_mangle_exist rules_mangle_counters rules_mangle_output_exist rules_mangle_output_counters rules_proxy_exist rules_proxy_counters rules_other_mark_exist + + table_exist=$(echo "$nft_check_json" | jq -r '.table_exist // 0') + rules_mangle_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_exist // 0') + rules_mangle_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_counters // 0') + rules_mangle_output_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_exist // 0') + rules_mangle_output_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_counters // 0') + rules_proxy_exist=$(echo "$nft_check_json" | jq -r '.rules_proxy_exist // 0') + rules_proxy_counters=$(echo "$nft_check_json" | jq -r '.rules_proxy_counters // 0') + rules_other_mark_exist=$(echo "$nft_check_json" | jq -r '.rules_other_mark_exist // 0') + + if [ "$table_exist" -eq 1 ]; then + print_global "✅ Table exist" + else + print_global "❌ Table exist" + fi + + if [ "$rules_mangle_exist" -eq 1 ]; then + print_global "✅ Rules mangle exist" + else + print_global "❌ Rules mangle exist" + fi + + if [ "$rules_mangle_counters" -eq 1 ]; then + print_global "✅ Rules mangle counters" + else + print_global "⚠️ Rules mangle counters" + fi + + if [ "$rules_mangle_output_exist" -eq 1 ]; then + print_global "✅ Rules mangle output exist" + else + print_global "❌ Rules mangle output exist" + fi + + if [ "$rules_mangle_output_counters" -eq 1 ]; then + print_global "✅ Rules mangle output counters" + else + print_global "⚠️ Rules mangle output counters" + fi + + if [ "$rules_proxy_exist" -eq 1 ]; then + print_global "✅ Rules proxy exist" + else + print_global "❌ Rules proxy exist" + fi + + if [ "$rules_proxy_counters" -eq 1 ]; then + print_global "✅ Rules proxy counters" + else + print_global "⚠️ Rules proxy counters" + fi + + if [ "$rules_other_mark_exist" -eq 1 ]; then + print_global "⚠️ Additional marking rules found:" + nft list ruleset | awk '/table inet '"$NFT_TABLE_NAME"'/{flag=1; next} /^table/{flag=0} !flag' | grep -E "mark set|meta mark" + else + print_global "✅ Additional marking rules found" + fi + else + print_global "❌ Failed to get NFT rules info" + fi print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_global "📄 Podkop config" show_config - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🔧 System check" + # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # print_global "🔧 System check" - if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then - print_global "❌ /etc/resolv.conf contains external nameserver:" - cat /etc/resolv.conf - echo "" - else - print_global "✅ /etc/resolv.conf" - fi + # if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then + # print_global "❌ /etc/resolv.conf contains external nameserver:" + # cat /etc/resolv.conf + # echo "" + # else + # print_global "✅ /etc/resolv.conf" + # fi - cachesize="$(uci get dhcp.@dnsmasq[0].cachesize 2> /dev/null)" - noresolv="$(uci get dhcp.@dnsmasq[0].noresolv 2> /dev/null)" - server="$(uci get dhcp.@dnsmasq[0].server 2> /dev/null)" - - if [ "$cachesize" != "0" ] || [ "$noresolv" != "1" ] || [ "$server" != "127.0.0.42" ]; then - print_global "❌ DHCP configuration differs from template. 📄 DHCP config:" - awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp - elif [ "$(uci get podkop.main.dont_touch_dhcp 2> /dev/null)" = "1" ]; then - print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:" - awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp - else - print_global "✅ /etc/config/dhcp" - fi - - if ! pgrep -f "sing-box" > /dev/null; then - print_global "❌ sing-box is not running" - else - print_global "✅ sing-box is running" - fi - - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🧱 NFT table" - check_nft + # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # print_global "🧱 NFT table" + # check_nft print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_global "📄 WAN config" @@ -2053,11 +2441,13 @@ global_check() { fi if uci show network | grep -q route_allowed_ips; then - uci show network | grep route_allowed_ips | cut -d"'" -f2 | while read -r value; do - if [ "$value" = "1" ]; then + uci show network | grep "wireguard_.*\.route_allowed_ips='1'" | cut -d'.' -f1-2 | while read -r peer_section; do + local allowed_ips + allowed_ips=$(uci get "${peer_section}.allowed_ips" 2> /dev/null) + + if [ "$allowed_ips" = "0.0.0.0/0" ]; then print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "⚠️ WG Route allowed IP enabled" - continue + print_global "⚠️ WG Route allowed IP enabled with 0.0.0.0/0" fi done fi @@ -2068,42 +2458,32 @@ global_check() { fi print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "➡️ DNS status" - dns_info=$(check_dns_available) - dns_type=$(echo "$dns_info" | jq -r '.dns_type') - dns_server=$(echo "$dns_info" | jq -r '.dns_server') - status=$(echo "$dns_info" | jq -r '.status') - print_global "$dns_type ($dns_server) is $status" + print_global "🥸 FakeIP status" - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🔁 FakeIP" + local fakeip_check_json + fakeip_check_json=$(check_fakeip) - print_global "➡️ DNS resolution: system DNS server" - nslookup -timeout=2 $FAKEIP_TEST_DOMAIN + if [ -n "$fakeip_check_json" ]; then + local fakeip_status - local working_resolver - working_resolver=$(find_working_resolver) - if [ -z "$working_resolver" ]; then - print_global "❌ No working external resolver found" + fakeip_status=$(echo "$fakeip_check_json" | jq -r '.fakeip // false') + + if [ "$fakeip_status" = "true" ]; then + print_global "✅ Router DNS is routed through sing-box" + else + print_global "⚠️ Router DNS is NOT routed through sing-box" + fi else - print_global "➡️ DNS resolution: external resolver ($working_resolver)" - nslookup -timeout=2 $FAKEIP_TEST_DOMAIN $working_resolver + print_global "❌ Failed to get FakeIP info" fi - print_global "➡️ DNS resolution: sing-box DNS server (127.0.0.42)" - local result - result=$(nslookup -timeout=2 $FAKEIP_TEST_DOMAIN 127.0.0.42 2>&1) - echo "$result" + local fakeip_address + fakeip_address=$(dig +short @127.0.0.42 $FAKEIP_TEST_DOMAIN) - if echo "$result" | grep -q "198.18"; then - print_global "✅ FakeIP is working correctly on router (198.18.x.x)" + if echo "$fakeip_address" | grep -q "^198\.18\."; then + print_global "✅ Sing-box works with FakeIP: $fakeip_address" else - print_global "❌ FakeIP test failed: Domain did not resolve to FakeIP range" - if ! pgrep -f "sing-box" > /dev/null; then - print_global " ❌ sing-box is not running" - else - print_global " 🤔 sing-box is running" - fi + print_global "❌ Sing-box does NOT work with FakeIP: $fakeip_address" fi } @@ -2116,17 +2496,16 @@ Available commands: stop Stop podkop service reload Reload podkop configuration restart Restart podkop service - enable Enable podkop autostart - disable Disable podkop autostart main Run main podkop process list_update Update domain lists check_proxy Check proxy connectivity check_nft Check NFT rules - check_github Check GitHub connectivity + check_nft_rules Check NFT rules status + check_sing_box Check sing-box installation and status check_logs Show podkop logs from system journal - check_sing_box_connections Show active sing-box connections check_sing_box_logs Show sing-box logs - check_dnsmasq Check DNSMasq configuration + check_fakeip Test FakeIP on router + clash_api Clash API interface for managing proxies and groups show_config Display current podkop configuration show_version Show podkop version show_sing_box_config Show sing-box configuration @@ -2134,6 +2513,7 @@ Available commands: show_system_info Show system information get_status Get podkop service status get_sing_box_status Get sing-box service status + get_system_info Get system information in JSON format check_dns_available Check DNS server availability global_check Run global system check EOF @@ -2164,20 +2544,23 @@ check_proxy) check_nft) check_nft ;; -check_github) - check_github +check_nft_rules) + check_nft_rules + ;; +check_sing_box) + check_sing_box ;; check_logs) check_logs ;; -check_sing_box_connections) - check_sing_box_connections - ;; check_sing_box_logs) check_sing_box_logs ;; -check_dnsmasq) - check_dnsmasq +check_fakeip) + check_fakeip + ;; +clash_api) + clash_api "$2" "$3" "$4" ;; show_config) show_config @@ -2200,6 +2583,9 @@ get_status) get_sing_box_status) get_sing_box_status ;; +get_system_info) + get_system_info + ;; check_dns_available) check_dns_available ;; @@ -2210,4 +2596,4 @@ global_check) show_help exit 1 ;; -esac \ No newline at end of file +esac diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 42e4156..6d98d79 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -38,15 +38,12 @@ SB_TPROXY_INBOUND_PORT=1602 SB_DNS_INBOUND_TAG="dns-in" SB_DNS_INBOUND_ADDRESS="127.0.0.42" SB_DNS_INBOUND_PORT=53 -SB_MIXED_INBOUND_TAG="mixed-in" SB_MIXED_INBOUND_ADDRESS="0.0.0.0" # TODO(ampetelin): maybe to determine address? -SB_MIXED_INBOUND_PORT=2080 SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in" SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1" SB_SERVICE_MIXED_INBOUND_PORT=4534 # Outbounds SB_DIRECT_OUTBOUND_TAG="direct-out" -SB_MAIN_OUTBOUND_TAG="main-out" # Route SB_REJECT_RULE_TAG="reject-rule-tag" # Experimental diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index a8d035a..7af83f2 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -128,7 +128,7 @@ get_ruleset_format_by_file_extension() { json) format="source" ;; srs) format="binary" ;; *) - log "Unsupported file extension: .$file_extension" + log "Unsupported file extension: .$file_extension" "error" return 1 ;; esac diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index bdff029..ffbe854 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -34,7 +34,7 @@ sing_box_cf_add_dns_server() { "$domain_resolver" "$detour") ;; *) - log "Unsupported DNS server type: $type" + log "Unsupported DNS server type: $type. Aborted." "fatal" exit 1 ;; esac @@ -66,6 +66,32 @@ sing_box_cf_add_proxy_outbound() { local scheme="${url%%://*}" case "$scheme" in + socks4 | socks4a | socks5) + local tag host port version userinfo username password udp_over_tcp + + tag=$(get_outbound_tag_by_section "$section") + host=$(url_get_host "$url") + port=$(url_get_port "$url") + version="${scheme#socks}" + if [ "$scheme" = "socks5" ]; then + userinfo=$(url_get_userinfo "$url") + if [ -n "$userinfo" ]; then + username="${userinfo%%:*}" + password="${userinfo#*:}" + fi + fi + config="$(sing_box_cm_add_socks_outbound \ + "$config" \ + "$tag" \ + "$host" \ + "$port" \ + "$version" \ + "$username" \ + "$password" \ + "" \ + "$([ "$udp_over_tcp" == "1" ] && echo 2)" # if udp_over_tcp is enabled, enable version 2 + )" + ;; vless) local tag host port uuid flow packet_encoding tag=$(get_outbound_tag_by_section "$section") @@ -121,7 +147,7 @@ sing_box_cf_add_proxy_outbound() { config=$(_add_outbound_transport "$config" "$tag" "$url") ;; *) - log "Unsupported proxy $scheme type" + log "Unsupported proxy $scheme type. Aborted." "fatal" exit 1 ;; esac diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index ce66424..f2b745a 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -449,12 +449,12 @@ sing_box_cm_add_direct_outbound() { } ####################################### -# Add a SOCKS5 outbound to the outbounds section of a sing-box JSON configuration. +# Add a SOCKS outbound to the outbounds section of a sing-box JSON configuration. # Arguments: # config: JSON configuration (string) # tag: string, identifier for the outbound -# server_address: string, IP address or hostname of the SOCKS5 server -# server_port: number, port of the SOCKS5 server +# server_address: string, IP address or hostname of the SOCKS server +# server_port: number, port of the SOCKS server # version: string, optional SOCKS version # username: string, optional username for authentication # password: string, optional password for authentication @@ -463,9 +463,9 @@ sing_box_cm_add_direct_outbound() { # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_add_socks5_outbound "$CONFIG" "socks5-out" "192.168.1.10" 1080) +# CONFIG=$(sing_box_cm_add_socks_outbound "$CONFIG" "socks5-out" "192.168.1.10" 1080) ####################################### -sing_box_cm_add_socks5_outbound() { +sing_box_cm_add_socks_outbound() { local config="$1" local tag="$2" local server_address="$3" @@ -644,12 +644,12 @@ sing_box_cm_add_trojan_outbound() { local network="$6" echo "$config" | jq \ - --arg tag "$tag" \ - --arg server_address "$server_address" \ - --arg server_port "$server_port" \ - --arg password "$password" \ - --arg network "$network" \ - '.outbounds += [( + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + --arg password "$password" \ + --arg network "$network" \ + '.outbounds += [( { type: "trojan", tag: $tag, @@ -969,6 +969,7 @@ sing_box_cm_add_selector_outbound() { # final: string, final outbound tag for unmatched traffic # auto_detect_interface: boolean, enable or disable automatic interface detection # default_domain_resolver: string, default DNS resolver for domain-based routing +# default_interface: string, default network interface to use when auto detection is disabled # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -979,18 +980,22 @@ sing_box_cm_configure_route() { local final="$2" local auto_detect_interface="$3" local default_domain_resolver="$4" + local default_interface="$5" echo "$config" | jq \ --arg final "$final" \ --argjson auto_detect_interface "$auto_detect_interface" \ --arg default_domain_resolver "$default_domain_resolver" \ + --arg default_interface "$default_interface" \ '.route = { rules: (.route.rules // []), rule_set: (.route.rule_set // []), final: $final, auto_detect_interface: $auto_detect_interface, default_domain_resolver: $default_domain_resolver - }' + } + + (if $default_interface != "" then { default_interface: $default_interface } else {} end) + ' } #######################################