diff --git a/.gitignore b/.gitignore index 703db4d..ff06e12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea fe-app-podkop/node_modules +fe-app-podkop/.env diff --git a/fe-app-podkop/eslint.config.js b/fe-app-podkop/eslint.config.js index 859f377..8ec3a34 100644 --- a/fe-app-podkop/eslint.config.js +++ b/fe-app-podkop/eslint.config.js @@ -7,7 +7,7 @@ export default [ js.configs.recommended, ...tseslint.configs.recommended, { - ignores: ['node_modules'], + ignores: ['node_modules', 'watch-upload.js'], }, { rules: { diff --git a/fe-app-podkop/package.json b/fe-app-podkop/package.json index 92b4f18..6241ec2 100644 --- a/fe-app-podkop/package.json +++ b/fe-app-podkop/package.json @@ -10,14 +10,19 @@ "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" + "ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build", + "watch:sftp": "node watch-upload.js" }, "devDependencies": { "@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", + "glob": "11.0.3", "prettier": "3.6.2", + "ssh2-sftp-client": "12.0.1", "tsup": "8.5.0", "typescript": "5.9.3", "typescript-eslint": "8.45.0", diff --git a/fe-app-podkop/src/clash/index.ts b/fe-app-podkop/src/clash/index.ts new file mode 100644 index 0000000..c3b7574 --- /dev/null +++ b/fe-app-podkop/src/clash/index.ts @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..601a433 --- /dev/null +++ b/fe-app-podkop/src/clash/methods/createBaseApiRequest.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..8f7135a --- /dev/null +++ b/fe-app-podkop/src/clash/methods/getConfig.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..f160bec --- /dev/null +++ b/fe-app-podkop/src/clash/methods/getGroupDelay.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..e465c58 --- /dev/null +++ b/fe-app-podkop/src/clash/methods/getProxies.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..119db9f --- /dev/null +++ b/fe-app-podkop/src/clash/methods/getVersion.ts @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..1feccdb --- /dev/null +++ b/fe-app-podkop/src/clash/methods/index.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..b7fffd9 --- /dev/null +++ b/fe-app-podkop/src/clash/methods/triggerLatencyTest.ts @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..16d1f55 --- /dev/null +++ b/fe-app-podkop/src/clash/methods/triggerProxySelector.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..a54a55f --- /dev/null +++ b/fe-app-podkop/src/clash/types.ts @@ -0,0 +1,53 @@ +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 deleted file mode 100644 index 154f4c5..0000000 --- a/fe-app-podkop/src/helpers/copyToClipboard.ts +++ /dev/null @@ -1,29 +0,0 @@ -interface CopyToClipboardResponse { - success: boolean; - message: string; -} - -export function copyToClipboard(text: string): CopyToClipboardResponse { - const textarea = document.createElement('textarea'); - textarea.value = text; - document.body.appendChild(textarea); - textarea.select(); - - try { - document.execCommand('copy'); - - return { - success: true, - message: 'Copied!', - }; - } catch (err) { - const error = err as Error; - - return { - success: false, - message: `Failed to copy: ${error.message}`, - }; - } finally { - document.body.removeChild(textarea); - } -} diff --git a/fe-app-podkop/src/helpers/getClashApiUrl.ts b/fe-app-podkop/src/helpers/getClashApiUrl.ts new file mode 100644 index 0000000..df52bec --- /dev/null +++ b/fe-app-podkop/src/helpers/getClashApiUrl.ts @@ -0,0 +1,11 @@ +export function getClashApiUrl(): string { + const { protocol, hostname } = window.location; + + return `${protocol}//${hostname}:9090`; +} + +export function getClashWsUrl(): string { + const { hostname } = window.location; + + return `ws://${hostname}:9090`; +} diff --git a/fe-app-podkop/src/helpers/getProxyUrlName.ts b/fe-app-podkop/src/helpers/getProxyUrlName.ts new file mode 100644 index 0000000..f903429 --- /dev/null +++ b/fe-app-podkop/src/helpers/getProxyUrlName.ts @@ -0,0 +1,13 @@ +export function getProxyUrlName(url: string) { + try { + const [_link, hash] = url.split('#'); + + if (!hash) { + return ''; + } + + return decodeURIComponent(hash); + } catch { + return ''; + } +} diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index 5569d6e..242f2e7 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -3,5 +3,7 @@ export * from './parseValueList'; export * from './injectGlobalStyles'; export * from './withTimeout'; export * from './executeShellCommand'; -export * from './copyToClipboard'; export * from './maskIP'; +export * from './getProxyUrlName'; +export * from './onMount'; +export * from './getClashApiUrl'; diff --git a/fe-app-podkop/src/helpers/onMount.ts b/fe-app-podkop/src/helpers/onMount.ts new file mode 100644 index 0000000..48ce5e8 --- /dev/null +++ b/fe-app-podkop/src/helpers/onMount.ts @@ -0,0 +1,30 @@ +export async function onMount(id: string): Promise { + 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, + }); + }); +} diff --git a/fe-app-podkop/src/helpers/prettyBytes.ts b/fe-app-podkop/src/helpers/prettyBytes.ts new file mode 100644 index 0000000..5572ccb --- /dev/null +++ b/fe-app-podkop/src/helpers/prettyBytes.ts @@ -0,0 +1,12 @@ +// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js +export function prettyBytes(n: number) { + const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + if (n < 1000) { + return n + ' B'; + } + const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); + n = Number((n / Math.pow(1000, exponent)).toPrecision(3)); + const unit = UNITS[exponent]; + return n + ' ' + unit; +} diff --git a/fe-app-podkop/src/helpers/withTimeout.ts b/fe-app-podkop/src/helpers/withTimeout.ts index 4475a55..f06108a 100644 --- a/fe-app-podkop/src/helpers/withTimeout.ts +++ b/fe-app-podkop/src/helpers/withTimeout.ts @@ -2,7 +2,7 @@ export async function withTimeout( promise: Promise, timeoutMs: number, operationName: string, - timeoutMessage = 'Operation timed out', + timeoutMessage = _('Operation timed out'), ): Promise { let timeoutId; const start = performance.now(); diff --git a/fe-app-podkop/src/luci.d.ts b/fe-app-podkop/src/luci.d.ts index 2b942de..01ff833 100644 --- a/fe-app-podkop/src/luci.d.ts +++ b/fe-app-podkop/src/luci.d.ts @@ -1,3 +1,15 @@ +type HtmlTag = keyof HTMLElementTagNameMap; + +type HtmlElement = HTMLElementTagNameMap[T]; + +type HtmlAttributes = Partial< + Omit, 'style' | 'children'> & { + style?: string | Partial; + class?: string; + onclick?: (event: MouseEvent) => void; + } +>; + declare global { const fs: { exec( @@ -10,6 +22,19 @@ declare global { code?: number; }>; }; + + const E: ( + type: T, + attr?: HtmlAttributes | null, + children?: (Node | string)[] | Node | string, + ) => HTMLElementTagNameMap[T]; + + const uci: { + load: (packages: string | string[]) => Promise; + sections: (conf: string, type?: string, cb?: () => void) => Promise; + }; + + const _ = (_key: string) => string; } export {}; diff --git a/fe-app-podkop/src/main.ts b/fe-app-podkop/src/main.ts index f3656c5..7d7e2b7 100644 --- a/fe-app-podkop/src/main.ts +++ b/fe-app-podkop/src/main.ts @@ -1,7 +1,10 @@ 'use strict'; 'require baseclass'; 'require fs'; +'require uci'; export * from './validators'; export * from './helpers'; +export * from './clash'; +export * from './podkop'; export * from './constants'; diff --git a/fe-app-podkop/src/podkop/index.ts b/fe-app-podkop/src/podkop/index.ts new file mode 100644 index 0000000..59309df --- /dev/null +++ b/fe-app-podkop/src/podkop/index.ts @@ -0,0 +1,3 @@ +export * from './methods'; +export * from './services'; +export * from './tabs'; diff --git a/fe-app-podkop/src/podkop/methods/getConfigSections.ts b/fe-app-podkop/src/podkop/methods/getConfigSections.ts new file mode 100644 index 0000000..d8883d4 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/getConfigSections.ts @@ -0,0 +1,5 @@ +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/getDashboardSections.ts new file mode 100644 index 0000000..c101926 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts @@ -0,0 +1,153 @@ +import { Podkop } from '../types'; +import { getConfigSections } from './getConfigSections'; +import { getClashProxies } from '../../clash'; +import { getProxyUrlName } from '../../helpers'; + +interface IGetDashboardSectionsResponse { + success: boolean; + data: Podkop.OutboundGroup[]; +} + +export async function getDashboardSections(): Promise { + 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`, + ); + + return { + withTagSelect: false, + code: outbound?.code || section['.name'], + displayName: section['.name'], + outbounds: [ + { + code: outbound?.code || section['.name'], + displayName: + getProxyUrlName(section.proxy_string) || + outbound?.value?.name || + '', + 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`, + ); + + return { + withTagSelect: false, + code: outbound?.code || section['.name'], + displayName: section['.name'], + outbounds: [ + { + code: outbound?.code || section['.name'], + displayName: + decodeURIComponent(JSON.parse(section.outbound_json)?.tag) || + outbound?.value?.name || + '', + 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.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, + }, + ], + }; + } + + return { + withTagSelect: false, + code: section['.name'], + displayName: section['.name'], + outbounds: [], + }; + }); + + return { + success: true, + data, + }; +} diff --git a/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts b/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts new file mode 100644 index 0000000..9286dda --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts @@ -0,0 +1,21 @@ +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: 1000, + }); + + 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 new file mode 100644 index 0000000..d65221e --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts @@ -0,0 +1,23 @@ +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: 1000, + }); + + 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 new file mode 100644 index 0000000..6b2c1f3 --- /dev/null +++ b/fe-app-podkop/src/podkop/methods/index.ts @@ -0,0 +1,4 @@ +export * from './getConfigSections'; +export * from './getDashboardSections'; +export * from './getPodkopStatus'; +export * from './getSingboxStatus'; diff --git a/fe-app-podkop/src/podkop/services/core.service.ts b/fe-app-podkop/src/podkop/services/core.service.ts new file mode 100644 index 0000000..4b7d827 --- /dev/null +++ b/fe-app-podkop/src/podkop/services/core.service.ts @@ -0,0 +1,13 @@ +import { TabServiceInstance } from './tab.service'; +import { store } from '../../store'; + +export function coreService() { + TabServiceInstance.onChange((activeId, tabs) => { + store.set({ + tabService: { + current: activeId || '', + all: tabs.map((tab) => tab.id), + }, + }); + }); +} diff --git a/fe-app-podkop/src/podkop/services/index.ts b/fe-app-podkop/src/podkop/services/index.ts new file mode 100644 index 0000000..4b776d2 --- /dev/null +++ b/fe-app-podkop/src/podkop/services/index.ts @@ -0,0 +1,2 @@ +export * from './tab.service'; +export * from './core.service'; diff --git a/fe-app-podkop/src/podkop/services/tab.service.ts b/fe-app-podkop/src/podkop/services/tab.service.ts new file mode 100644 index 0000000..88614ff --- /dev/null +++ b/fe-app-podkop/src/podkop/services/tab.service.ts @@ -0,0 +1,92 @@ +type TabInfo = { + el: HTMLElement; + id: string; + active: boolean; +}; + +type TabChangeCallback = (activeId: string | null, allTabs: TabInfo[]) => void; + +export class TabService { + private static instance: TabService; + private observer: MutationObserver | null = null; + private callback?: TabChangeCallback; + private lastActiveId: string | null = null; + + private constructor() { + this.init(); + } + + public static getInstance(): TabService { + if (!TabService.instance) { + TabService.instance = new TabService(); + } + return TabService.instance; + } + + private init() { + this.observer = new MutationObserver(() => this.handleMutations()); + this.observer.observe(document.body, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['class'], + }); + + // initial check + this.notify(); + } + + private handleMutations() { + this.notify(); + } + + private getTabsInfo(): TabInfo[] { + const tabs = Array.from( + document.querySelectorAll('.cbi-tab, .cbi-tab-disabled'), + ); + return tabs.map((el) => ({ + el, + id: el.dataset.tab || '', + active: + el.classList.contains('cbi-tab') && + !el.classList.contains('cbi-tab-disabled'), + })); + } + + private getActiveTabId(): string | null { + const active = document.querySelector( + '.cbi-tab:not(.cbi-tab-disabled)', + ); + return active?.dataset.tab || null; + } + + private notify() { + const tabs = this.getTabsInfo(); + const activeId = this.getActiveTabId(); + + if (activeId !== this.lastActiveId) { + this.lastActiveId = activeId; + this.callback?.(activeId, tabs); + } + } + + public onChange(callback: TabChangeCallback) { + this.callback = callback; + this.notify(); + } + + public getAllTabs(): TabInfo[] { + return this.getTabsInfo(); + } + + public getActiveTab(): string | null { + return this.getActiveTabId(); + } + + public disconnect() { + this.observer?.disconnect(); + this.observer = null; + } +} + +export const TabServiceInstance = TabService.getInstance(); diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/index.ts b/fe-app-podkop/src/podkop/tabs/dashboard/index.ts new file mode 100644 index 0000000..898949a --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/index.ts @@ -0,0 +1,2 @@ +export * from './renderDashboard'; +export * from './initDashboardController'; diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts b/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts new file mode 100644 index 0000000..c630616 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts @@ -0,0 +1,393 @@ +import { + getDashboardSections, + getPodkopStatus, + getSingboxStatus, +} from '../../methods'; +import { getClashWsUrl, onMount } 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'; + +// Fetchers + +async function fetchDashboardSections() { + const prev = store.get().sectionsWidget; + + store.set({ + sectionsWidget: { + ...prev, + failed: false, + }, + }); + + const { data, success } = await getDashboardSections(); + + store.set({ + sectionsWidget: { + loading: false, + failed: !success, + data, + }, + }); +} + +async function fetchServicesInfo() { + const [podkop, singbox] = await Promise.all([ + getPodkopStatus(), + getSingboxStatus(), + ]); + + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.running, podkop: podkop.enabled }, + }, + }); +} + +async function connectToClashSockets() { + socket.subscribe( + `${getClashWsUrl()}/traffic?token=`, + (msg) => { + const parsedMsg = JSON.parse(msg); + + store.set({ + bandwidthWidget: { + loading: false, + failed: false, + data: { up: parsedMsg.up, down: parsedMsg.down }, + }, + }); + }, + (_err) => { + store.set({ + bandwidthWidget: { + loading: false, + failed: true, + data: { up: 0, down: 0 }, + }, + }); + }, + ); + + socket.subscribe( + `${getClashWsUrl()}/connections?token=`, + (msg) => { + const parsedMsg = JSON.parse(msg); + + store.set({ + trafficTotalWidget: { + loading: false, + failed: false, + data: { + downloadTotal: parsedMsg.downloadTotal, + uploadTotal: parsedMsg.uploadTotal, + }, + }, + systemInfoWidget: { + loading: false, + failed: false, + data: { + connections: parsedMsg.connections?.length, + memory: parsedMsg.memory, + }, + }, + }); + }, + (_err) => { + store.set({ + trafficTotalWidget: { + loading: false, + failed: true, + data: { downloadTotal: 0, uploadTotal: 0 }, + }, + systemInfoWidget: { + loading: false, + failed: true, + data: { + connections: 0, + memory: 0, + }, + }, + }); + }, + ); +} + +// Handlers + +async function handleChooseOutbound(selector: string, tag: string) { + await triggerProxySelector(selector, tag); + await fetchDashboardSections(); +} + +async function handleTestGroupLatency(tag: string) { + await triggerLatencyGroupTest(tag); + await fetchDashboardSections(); +} + +async function handleTestProxyLatency(tag: string) { + await triggerLatencyProxyTest(tag); + await fetchDashboardSections(); +} + +function replaceTestLatencyButtonsWithSkeleton() { + document + .querySelectorAll('.dashboard-sections-grid-item-test-latency') + .forEach((el) => { + const newDiv = document.createElement('div'); + newDiv.className = 'skeleton'; + newDiv.style.width = '99px'; + newDiv.style.height = '28px'; + el.replaceWith(newDiv); + }); +} + +// Renderer + +async function renderSectionsWidget() { + console.log('renderSectionsWidget'); + const sectionsWidget = store.get().sectionsWidget; + const container = document.getElementById('dashboard-sections-grid'); + + if (sectionsWidget.loading || sectionsWidget.failed) { + const renderedWidget = renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section: { + code: '', + displayName: '', + outbounds: [], + withTagSelect: false, + }, + onTestLatency: () => {}, + onChooseOutbound: () => {}, + }); + return container!.replaceChildren(renderedWidget); + } + + const renderedWidgets = sectionsWidget.data.map((section) => + renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section, + onTestLatency: (tag) => { + replaceTestLatencyButtonsWithSkeleton(); + + if (section.withTagSelect) { + return handleTestGroupLatency(tag); + } + + return handleTestProxyLatency(tag); + }, + onChooseOutbound: (selector, tag) => { + handleChooseOutbound(selector, tag); + }, + }), + ); + + return container!.replaceChildren(...renderedWidgets); +} + +async function renderBandwidthWidget() { + console.log('renderBandwidthWidget'); + const traffic = store.get().bandwidthWidget; + + const container = document.getElementById('dashboard-widget-traffic'); + + if (traffic.loading || traffic.failed) { + const renderedWidget = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: '', + items: [], + }); + + return container!.replaceChildren(renderedWidget); + } + + const renderedWidget = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: _('Traffic'), + items: [ + { key: _('Uplink'), value: `${prettyBytes(traffic.data.up)}/s` }, + { key: _('Downlink'), value: `${prettyBytes(traffic.data.down)}/s` }, + ], + }); + + container!.replaceChildren(renderedWidget); +} + +async function renderTrafficTotalWidget() { + console.log('renderTrafficTotalWidget'); + const trafficTotalWidget = store.get().trafficTotalWidget; + + const container = document.getElementById('dashboard-widget-traffic-total'); + + if (trafficTotalWidget.loading || trafficTotalWidget.failed) { + const renderedWidget = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: '', + items: [], + }); + + return container!.replaceChildren(renderedWidget); + } + + const renderedWidget = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: _('Traffic Total'), + items: [ + { + key: _('Uplink'), + value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)), + }, + { + key: _('Downlink'), + value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)), + }, + ], + }); + + container!.replaceChildren(renderedWidget); +} + +async function renderSystemInfoWidget() { + console.log('renderSystemInfoWidget'); + const systemInfoWidget = store.get().systemInfoWidget; + + const container = document.getElementById('dashboard-widget-system-info'); + + if (systemInfoWidget.loading || systemInfoWidget.failed) { + const renderedWidget = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: '', + items: [], + }); + + return container!.replaceChildren(renderedWidget); + } + + const renderedWidget = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: _('System info'), + items: [ + { + key: _('Active Connections'), + value: String(systemInfoWidget.data.connections), + }, + { + key: _('Memory Usage'), + value: String(prettyBytes(systemInfoWidget.data.memory)), + }, + ], + }); + + container!.replaceChildren(renderedWidget); +} + +async function renderServicesInfoWidget() { + console.log('renderServicesInfoWidget'); + const servicesInfoWidget = store.get().servicesInfoWidget; + + const container = document.getElementById('dashboard-widget-service-info'); + + if (servicesInfoWidget.loading || servicesInfoWidget.failed) { + const renderedWidget = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: '', + items: [], + }); + + return container!.replaceChildren(renderedWidget); + } + + const renderedWidget = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: _('Services info'), + items: [ + { + key: _('Podkop'), + value: servicesInfoWidget.data.podkop + ? _('✔ Enabled') + : _('✘ Disabled'), + attributes: { + class: servicesInfoWidget.data.podkop + ? 'pdk_dashboard-page__widgets-section__item__row--success' + : 'pdk_dashboard-page__widgets-section__item__row--error', + }, + }, + { + key: _('Sing-box'), + value: servicesInfoWidget.data.singbox + ? _('✔ Running') + : _('✘ Stopped'), + attributes: { + class: servicesInfoWidget.data.singbox + ? 'pdk_dashboard-page__widgets-section__item__row--success' + : 'pdk_dashboard-page__widgets-section__item__row--error', + }, + }, + ], + }); + + container!.replaceChildren(renderedWidget); +} + +async function onStoreUpdate( + next: StoreType, + prev: StoreType, + diff: Partial, +) { + if (diff.sectionsWidget) { + renderSectionsWidget(); + } + + if (diff.bandwidthWidget) { + renderBandwidthWidget(); + } + + if (diff.trafficTotalWidget) { + renderTrafficTotalWidget(); + } + + if (diff.systemInfoWidget) { + renderSystemInfoWidget(); + } + + if (diff.servicesInfoWidget) { + renderServicesInfoWidget(); + } +} + +export async function initDashboardController(): Promise { + onMount('dashboard-status').then(() => { + // Remove old listener + store.unsubscribe(onStoreUpdate); + // Clear store + store.reset(); + + // Add new listener + store.subscribe(onStoreUpdate); + + // Initial sections fetch + fetchDashboardSections(); + fetchServicesInfo(); + connectToClashSockets(); + }); +} diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts new file mode 100644 index 0000000..b4151e2 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts @@ -0,0 +1,54 @@ +import { renderSections } from './renderSections'; +import { renderWidget } from './renderWidget'; + +export function renderDashboard() { + return E( + 'div', + { + id: 'dashboard-status', + class: 'pdk_dashboard-page', + }, + [ + // Widgets section + E('div', { class: 'pdk_dashboard-page__widgets-section' }, [ + E( + 'div', + { id: 'dashboard-widget-traffic' }, + renderWidget({ loading: true, failed: false, title: '', items: [] }), + ), + E( + 'div', + { id: 'dashboard-widget-traffic-total' }, + renderWidget({ loading: true, failed: false, title: '', items: [] }), + ), + E( + 'div', + { id: 'dashboard-widget-system-info' }, + renderWidget({ loading: true, failed: false, title: '', items: [] }), + ), + E( + 'div', + { id: 'dashboard-widget-service-info' }, + renderWidget({ loading: true, failed: false, title: '', items: [] }), + ), + ]), + // All outbounds + E( + 'div', + { id: 'dashboard-sections-grid' }, + renderSections({ + loading: true, + failed: false, + section: { + code: '', + displayName: '', + outbounds: [], + withTagSelect: false, + }, + onTestLatency: () => {}, + onChooseOutbound: () => {}, + }), + ), + ], + ); +} diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts new file mode 100644 index 0000000..4501ae7 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts @@ -0,0 +1,125 @@ +import { Podkop } from '../../types'; + +interface IRenderSectionsProps { + loading: boolean; + failed: boolean; + section: Podkop.OutboundGroup; + onTestLatency: (tag: string) => void; + onChooseOutbound: (selector: string, tag: string) => void; +} + +function renderFailedState() { + return E( + 'div', + { + class: 'pdk_dashboard-page__outbound-section centered', + style: 'height: 127px', + }, + E('span', {}, _('Dashboard currently unavailable')), + ); +} + +function renderLoadingState() { + return E('div', { + id: 'dashboard-sections-grid-skeleton', + class: 'pdk_dashboard-page__outbound-section skeleton', + style: 'height: 127px', + }); +} + +export function renderDefaultState({ + section, + onChooseOutbound, + onTestLatency, +}: IRenderSectionsProps) { + function testLatency() { + if (section.withTagSelect) { + return onTestLatency(section.code); + } + + if (section.outbounds.length) { + return onTestLatency(section.outbounds[0].code); + } + } + + function renderOutbound(outbound: Podkop.Outbound) { + function getLatencyClass() { + if (!outbound.latency) { + return 'pdk_dashboard-page__outbound-grid__item__latency--empty'; + } + + if (outbound.latency < 200) { + return 'pdk_dashboard-page__outbound-grid__item__latency--green'; + } + + if (outbound.latency < 400) { + return 'pdk_dashboard-page__outbound-grid__item__latency--yellow'; + } + + return 'pdk_dashboard-page__outbound-grid__item__latency--red'; + } + + return E( + 'div', + { + class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`, + click: () => + section.withTagSelect && + onChooseOutbound(section.code, outbound.code), + }, + [ + E('b', {}, outbound.displayName), + E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [ + E( + 'div', + { class: 'pdk_dashboard-page__outbound-grid__item__type' }, + outbound.type, + ), + E( + 'div', + { class: getLatencyClass() }, + outbound.latency ? `${outbound.latency}ms` : 'N/A', + ), + ]), + ], + ); + } + + return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [ + // Title with test latency + E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [ + E( + 'div', + { + class: 'pdk_dashboard-page__outbound-section__title-section__title', + }, + section.displayName, + ), + E( + 'button', + { + class: 'btn dashboard-sections-grid-item-test-latency', + click: () => testLatency(), + }, + 'Test latency', + ), + ]), + E( + 'div', + { class: 'pdk_dashboard-page__outbound-grid' }, + section.outbounds.map((outbound) => renderOutbound(outbound)), + ), + ]); +} + +export function renderSections(props: IRenderSectionsProps) { + if (props.failed) { + return renderFailedState(); + } + + if (props.loading) { + return renderLoadingState(); + } + + return renderDefaultState(props); +} diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts new file mode 100644 index 0000000..25216c0 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts @@ -0,0 +1,78 @@ +interface IRenderWidgetProps { + loading: boolean; + failed: boolean; + title: string; + items: Array<{ + key: string; + value: string; + attributes?: { + class?: string; + }; + }>; +} + +function renderFailedState() { + return E( + 'div', + { + id: '', + style: 'height: 78px', + class: 'pdk_dashboard-page__widgets-section__item centered', + }, + _('Currently unavailable'), + ); +} + +function renderLoadingState() { + return E( + 'div', + { + id: '', + style: 'height: 78px', + class: 'pdk_dashboard-page__widgets-section__item skeleton', + }, + '', + ); +} + +function renderDefaultState({ title, items }: IRenderWidgetProps) { + return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [ + E( + 'b', + { class: 'pdk_dashboard-page__widgets-section__item__title' }, + title, + ), + ...items.map((item) => + E( + 'div', + { + class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ''}`, + }, + [ + E( + 'span', + { class: 'pdk_dashboard-page__widgets-section__item__row__key' }, + `${item.key}: `, + ), + E( + 'span', + { class: 'pdk_dashboard-page__widgets-section__item__row__value' }, + item.value, + ), + ], + ), + ), + ]); +} + +export function renderWidget(props: IRenderWidgetProps) { + if (props.loading) { + return renderLoadingState(); + } + + if (props.failed) { + return renderFailedState(); + } + + return renderDefaultState(props); +} diff --git a/fe-app-podkop/src/podkop/tabs/index.ts b/fe-app-podkop/src/podkop/tabs/index.ts new file mode 100644 index 0000000..b58b6c9 --- /dev/null +++ b/fe-app-podkop/src/podkop/tabs/index.ts @@ -0,0 +1 @@ +export * from './dashboard'; diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts new file mode 100644 index 0000000..531f648 --- /dev/null +++ b/fe-app-podkop/src/podkop/types.ts @@ -0,0 +1,56 @@ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Podkop { + export interface Outbound { + code: string; + displayName: string; + latency: number; + type: string; + selected: boolean; + } + + export interface OutboundGroup { + withTagSelect: boolean; + code: string; + displayName: string; + outbounds: Outbound[]; + } + + export interface ConfigProxyUrlTestSection { + mode: 'proxy'; + proxy_config_type: 'urltest'; + urltest_proxy_links: string[]; + } + + export interface ConfigProxyUrlSection { + mode: 'proxy'; + proxy_config_type: 'url'; + proxy_string: string; + } + + export interface ConfigProxyOutboundSection { + mode: 'proxy'; + proxy_config_type: 'outbound'; + outbound_json: string; + } + + export interface ConfigVpnSection { + mode: 'vpn'; + interface: string; + } + + export interface ConfigBlockSection { + mode: 'block'; + } + + export type ConfigBaseSection = + | ConfigProxyUrlTestSection + | ConfigProxyUrlSection + | ConfigProxyOutboundSection + | ConfigVpnSection + | ConfigBlockSection; + + export type ConfigSection = ConfigBaseSection & { + '.name': string; + '.type': 'main' | 'extra'; + }; +} diff --git a/fe-app-podkop/src/socket.ts b/fe-app-podkop/src/socket.ts new file mode 100644 index 0000000..5a401b8 --- /dev/null +++ b/fe-app-podkop/src/socket.ts @@ -0,0 +1,121 @@ +// eslint-disable-next-line +type Listener = (data: any) => void; +type ErrorListener = (error: Event | string) => void; + +class SocketManager { + private static instance: SocketManager; + private sockets = new Map(); + private listeners = new Map>(); + private connected = new Map(); + private errorListeners = new Map>(); + + private constructor() {} + + static getInstance(): SocketManager { + if (!SocketManager.instance) { + SocketManager.instance = new SocketManager(); + } + return SocketManager.instance; + } + + connect(url: string): void { + 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, new Set()); + this.errorListeners.set(url, 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: string, listener: Listener, onError?: ErrorListener): void { + if (!this.sockets.has(url)) { + this.connect(url); + } + + this.listeners.get(url)?.add(listener); + + if (onError) { + this.errorListeners.get(url)?.add(onError); + } + } + + unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void { + this.listeners.get(url)?.delete(listener); + if (onError) { + this.errorListeners.get(url)?.delete(onError); + } + } + + // eslint-disable-next-line + send(url: string, data: any): void { + 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: string): void { + 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(): void { + for (const url of this.sockets.keys()) { + this.disconnect(url); + } + } + + private triggerError(url: string, err: Event | string): void { + 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); + } + } + } + } +} + +export const socket = SocketManager.getInstance(); diff --git a/fe-app-podkop/src/store.ts b/fe-app-podkop/src/store.ts new file mode 100644 index 0000000..4f5f4e8 --- /dev/null +++ b/fe-app-podkop/src/store.ts @@ -0,0 +1,179 @@ +import { Podkop } from './podkop/types'; + +function jsonStableStringify(obj: T): string { + return JSON.stringify(obj, (_, value) => { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return Object.keys(value) + .sort() + .reduce( + (acc, key) => { + acc[key] = value[key]; + return acc; + }, + {} as Record, + ); + } + return value; + }); +} + +function jsonEqual(a: A, b: B): boolean { + try { + return jsonStableStringify(a) === jsonStableStringify(b); + } catch { + return false; + } +} + +type Listener = (next: T, prev: T, diff: Partial) => void; + +// eslint-disable-next-line +class Store> { + private value: T; + private readonly initial: T; + private listeners = new Set>(); + private lastHash = ''; + + constructor(initial: T) { + this.value = initial; + this.initial = structuredClone(initial); + this.lastHash = jsonStableStringify(initial); + } + + get(): T { + return this.value; + } + + set(next: Partial): void { + const prev = this.value; + const merged = { ...prev, ...next }; + + if (jsonEqual(prev, merged)) return; + + this.value = merged; + this.lastHash = jsonStableStringify(merged); + + const diff: Partial = {}; + for (const key in merged) { + if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key]; + } + + this.listeners.forEach((cb) => cb(this.value, prev, diff)); + } + + reset(): void { + const prev = this.value; + const next = structuredClone(this.initial); + + if (jsonEqual(prev, next)) return; + + this.value = next; + this.lastHash = jsonStableStringify(next); + + const diff: Partial = {}; + for (const key in next) { + if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; + } + + this.listeners.forEach((cb) => cb(this.value, prev, diff)); + } + + subscribe(cb: Listener): () => void { + this.listeners.add(cb); + cb(this.value, this.value, {}); + return () => this.listeners.delete(cb); + } + + unsubscribe(cb: Listener): void { + this.listeners.delete(cb); + } + + patch(key: K, value: T[K]): void { + this.set({ [key]: value } as unknown as Partial); + } + + getKey(key: K): T[K] { + return this.value[key]; + } + + subscribeKey( + key: K, + cb: (value: T[K]) => void, + ): () => void { + let prev = this.value[key]; + const wrapper: Listener = (val) => { + if (!jsonEqual(val[key], prev)) { + prev = val[key]; + cb(val[key]); + } + }; + this.listeners.add(wrapper); + return () => this.listeners.delete(wrapper); + } +} + +export interface StoreType { + tabService: { + current: string; + all: string[]; + }; + bandwidthWidget: { + loading: boolean; + failed: boolean; + data: { up: number; down: number }; + }; + trafficTotalWidget: { + loading: boolean; + failed: boolean; + data: { downloadTotal: number; uploadTotal: number }; + }; + systemInfoWidget: { + loading: boolean; + failed: boolean; + data: { connections: number; memory: number }; + }; + servicesInfoWidget: { + loading: boolean; + failed: boolean; + data: { singbox: number; podkop: number }; + }; + sectionsWidget: { + loading: boolean; + failed: boolean; + data: Podkop.OutboundGroup[]; + }; +} + +const initialStore: StoreType = { + tabService: { + current: '', + all: [], + }, + bandwidthWidget: { + loading: true, + failed: false, + data: { up: 0, down: 0 }, + }, + trafficTotalWidget: { + loading: true, + failed: false, + data: { downloadTotal: 0, uploadTotal: 0 }, + }, + systemInfoWidget: { + loading: true, + failed: false, + data: { connections: 0, memory: 0 }, + }, + servicesInfoWidget: { + loading: true, + failed: false, + data: { singbox: 0, podkop: 0 }, + }, + sectionsWidget: { + loading: true, + failed: false, + data: [], + }, +}; + +export const store = new Store(initialStore); diff --git a/fe-app-podkop/src/styles.ts b/fe-app-podkop/src/styles.ts index 6613f9b..b135ef5 100644 --- a/fe-app-podkop/src/styles.ts +++ b/fe-app-podkop/src/styles.ts @@ -23,4 +23,155 @@ export const GlobalStyles = ` #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%; + } +} `; diff --git a/fe-app-podkop/src/validators/validateDns.ts b/fe-app-podkop/src/validators/validateDns.ts index f779d22..260ec18 100644 --- a/fe-app-podkop/src/validators/validateDns.ts +++ b/fe-app-podkop/src/validators/validateDns.ts @@ -4,20 +4,21 @@ import { ValidationResult } from './types'; export function validateDNS(value: string): ValidationResult { if (!value) { - return { valid: false, message: 'DNS server address cannot be empty' }; + return { valid: false, message: _('DNS server address cannot be empty') }; } if (validateIPV4(value).valid) { - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } if (validateDomain(value).valid) { - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } return { valid: false, - message: + message: _( 'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH', + ), }; } diff --git a/fe-app-podkop/src/validators/validateDomain.ts b/fe-app-podkop/src/validators/validateDomain.ts index 343fd86..fdebefa 100644 --- a/fe-app-podkop/src/validators/validateDomain.ts +++ b/fe-app-podkop/src/validators/validateDomain.ts @@ -5,7 +5,7 @@ export function validateDomain(domain: string): ValidationResult { /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; if (!domainRegex.test(domain)) { - return { valid: false, message: 'Invalid domain address' }; + return { valid: false, message: _('Invalid domain address') }; } const hostname = domain.split('/')[0]; @@ -14,8 +14,8 @@ export function validateDomain(domain: string): ValidationResult { const atLeastOneInvalidPart = parts.some((part) => part.length > 63); if (atLeastOneInvalidPart) { - return { valid: false, message: 'Invalid domain address' }; + return { valid: false, message: _('Invalid domain address') }; } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } diff --git a/fe-app-podkop/src/validators/validateIp.ts b/fe-app-podkop/src/validators/validateIp.ts index 88ab1f0..78c154d 100644 --- a/fe-app-podkop/src/validators/validateIp.ts +++ b/fe-app-podkop/src/validators/validateIp.ts @@ -5,8 +5,8 @@ export function validateIPV4(ip: string): ValidationResult { /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; if (ipRegex.test(ip)) { - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } - return { valid: false, message: 'Invalid IP address' }; + return { valid: false, message: _('Invalid IP address') }; } diff --git a/fe-app-podkop/src/validators/validateOutboundJson.ts b/fe-app-podkop/src/validators/validateOutboundJson.ts index c768543..822662b 100644 --- a/fe-app-podkop/src/validators/validateOutboundJson.ts +++ b/fe-app-podkop/src/validators/validateOutboundJson.ts @@ -8,13 +8,14 @@ export function validateOutboundJson(value: string): ValidationResult { if (!parsed.type || !parsed.server || !parsed.server_port) { return { valid: false, - message: + message: _( 'Outbound JSON must contain at least "type", "server" and "server_port" fields', + ), }; } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } catch { - return { valid: false, message: 'Invalid JSON format' }; + return { valid: false, message: _('Invalid JSON format') }; } } diff --git a/fe-app-podkop/src/validators/validatePath.ts b/fe-app-podkop/src/validators/validatePath.ts index 9da07ba..045601e 100644 --- a/fe-app-podkop/src/validators/validatePath.ts +++ b/fe-app-podkop/src/validators/validatePath.ts @@ -4,7 +4,7 @@ export function validatePath(value: string): ValidationResult { if (!value) { return { valid: false, - message: 'Path cannot be empty', + message: _('Path cannot be empty'), }; } @@ -19,7 +19,8 @@ export function validatePath(value: string): ValidationResult { return { valid: false, - message: + message: _( 'Invalid path format. Path must start with "/" and contain valid characters', + ), }; } diff --git a/fe-app-podkop/src/validators/validateProxyUrl.ts b/fe-app-podkop/src/validators/validateProxyUrl.ts index b9ef593..ec3fe47 100644 --- a/fe-app-podkop/src/validators/validateProxyUrl.ts +++ b/fe-app-podkop/src/validators/validateProxyUrl.ts @@ -19,6 +19,6 @@ export function validateProxyUrl(url: string): ValidationResult { return { valid: false, - message: 'URL must start with vless:// or ss:// or trojan://', + message: _('URL must start with vless:// or ss:// or trojan://'), }; } diff --git a/fe-app-podkop/src/validators/validateShadowsocksUrl.ts b/fe-app-podkop/src/validators/validateShadowsocksUrl.ts index 68081a7..29bd193 100644 --- a/fe-app-podkop/src/validators/validateShadowsocksUrl.ts +++ b/fe-app-podkop/src/validators/validateShadowsocksUrl.ts @@ -5,7 +5,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!url.startsWith('ss://')) { return { valid: false, - message: 'Invalid Shadowsocks URL: must start with ss://', + message: _('Invalid Shadowsocks URL: must start with ss://'), }; } @@ -13,7 +13,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!url || /\s/.test(url)) { return { valid: false, - message: 'Invalid Shadowsocks URL: must not contain spaces', + message: _('Invalid Shadowsocks URL: must not contain spaces'), }; } @@ -24,7 +24,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!encryptedPart) { return { valid: false, - message: 'Invalid Shadowsocks URL: missing credentials', + message: _('Invalid Shadowsocks URL: missing credentials'), }; } @@ -34,16 +34,18 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!decoded.includes(':')) { return { valid: false, - message: + message: _( 'Invalid Shadowsocks URL: decoded credentials must contain method:password', + ), }; } } catch (_e) { if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) { return { valid: false, - message: + message: _( 'Invalid Shadowsocks URL: missing method and password separator ":"', + ), }; } } @@ -53,7 +55,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!serverPart) { return { valid: false, - message: 'Invalid Shadowsocks URL: missing server address', + message: _('Invalid Shadowsocks URL: missing server address'), }; } @@ -62,14 +64,17 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (!server) { return { valid: false, - message: 'Invalid Shadowsocks URL: missing server', + message: _('Invalid Shadowsocks URL: missing server'), }; } const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; if (!port) { - return { valid: false, message: 'Invalid Shadowsocks URL: missing port' }; + return { + valid: false, + message: _('Invalid Shadowsocks URL: missing port'), + }; } const portNum = parseInt(port, 10); @@ -77,12 +82,15 @@ export function validateShadowsocksUrl(url: string): ValidationResult { if (isNaN(portNum) || portNum < 1 || portNum > 65535) { return { valid: false, - message: 'Invalid port number. Must be between 1 and 65535', + message: _('Invalid port number. Must be between 1 and 65535'), }; } } catch (_e) { - return { valid: false, message: 'Invalid Shadowsocks URL: parsing failed' }; + return { + valid: false, + message: _('Invalid Shadowsocks URL: parsing failed'), + }; } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } diff --git a/fe-app-podkop/src/validators/validateSubnet.ts b/fe-app-podkop/src/validators/validateSubnet.ts index 6f3e2b9..e7974a2 100644 --- a/fe-app-podkop/src/validators/validateSubnet.ts +++ b/fe-app-podkop/src/validators/validateSubnet.ts @@ -8,14 +8,14 @@ export function validateSubnet(value: string): ValidationResult { if (!subnetRegex.test(value)) { return { valid: false, - message: 'Invalid format. Use X.X.X.X or X.X.X.X/Y', + message: _('Invalid format. Use X.X.X.X or X.X.X.X/Y'), }; } const [ip, cidr] = value.split('/'); if (ip === '0.0.0.0') { - return { valid: false, message: 'IP address 0.0.0.0 is not allowed' }; + return { valid: false, message: _('IP address 0.0.0.0 is not allowed') }; } const ipCheck = validateIPV4(ip); @@ -30,10 +30,10 @@ export function validateSubnet(value: string): ValidationResult { if (cidrNum < 0 || cidrNum > 32) { return { valid: false, - message: 'CIDR must be between 0 and 32', + message: _('CIDR must be between 0 and 32'), }; } } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } diff --git a/fe-app-podkop/src/validators/validateTrojanUrl.ts b/fe-app-podkop/src/validators/validateTrojanUrl.ts index f79536c..8e9e627 100644 --- a/fe-app-podkop/src/validators/validateTrojanUrl.ts +++ b/fe-app-podkop/src/validators/validateTrojanUrl.ts @@ -5,14 +5,14 @@ export function validateTrojanUrl(url: string): ValidationResult { if (!url.startsWith('trojan://')) { return { valid: false, - message: 'Invalid Trojan URL: must start with trojan://', + message: _('Invalid Trojan URL: must start with trojan://'), }; } if (!url || /\s/.test(url)) { return { valid: false, - message: 'Invalid Trojan URL: must not contain spaces', + message: _('Invalid Trojan URL: must not contain spaces'), }; } @@ -22,12 +22,14 @@ export function validateTrojanUrl(url: string): ValidationResult { if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { return { valid: false, - message: 'Invalid Trojan URL: must contain username, hostname and port', + message: _( + 'Invalid Trojan URL: must contain username, hostname and port', + ), }; } } catch (_e) { - return { valid: false, message: 'Invalid Trojan URL: parsing failed' }; + return { valid: false, message: _('Invalid Trojan URL: parsing failed') }; } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } diff --git a/fe-app-podkop/src/validators/validateUrl.ts b/fe-app-podkop/src/validators/validateUrl.ts index 5b6d522..dd2c88e 100644 --- a/fe-app-podkop/src/validators/validateUrl.ts +++ b/fe-app-podkop/src/validators/validateUrl.ts @@ -10,11 +10,11 @@ export function validateUrl( if (!protocols.includes(parsedUrl.protocol)) { return { valid: false, - message: `URL must use one of the following protocols: ${protocols.join(', ')}`, + message: `${_('URL must use one of the following protocols:')} ${protocols.join(', ')}`, }; } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } catch (_e) { - return { valid: false, message: 'Invalid URL format' }; + return { valid: false, message: _('Invalid URL format') }; } } diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index e74ffa6..73746e4 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -1,6 +1,5 @@ import { ValidationResult } from './types'; -// TODO refactor current validation and add tests export function validateVlessUrl(url: string): ValidationResult { try { const parsedUrl = new URL(url); @@ -8,27 +7,27 @@ export function validateVlessUrl(url: string): ValidationResult { if (!url || /\s/.test(url)) { return { valid: false, - message: 'Invalid VLESS URL: must not contain spaces', + message: _('Invalid VLESS URL: must not contain spaces'), }; } if (parsedUrl.protocol !== 'vless:') { return { valid: false, - message: 'Invalid VLESS URL: must start with vless://', + message: _('Invalid VLESS URL: must start with vless://'), }; } if (!parsedUrl.username) { - return { valid: false, message: 'Invalid VLESS URL: missing UUID' }; + return { valid: false, message: _('Invalid VLESS URL: missing UUID') }; } if (!parsedUrl.hostname) { - return { valid: false, message: 'Invalid VLESS URL: missing server' }; + return { valid: false, message: _('Invalid VLESS URL: missing server') }; } if (!parsedUrl.port) { - return { valid: false, message: 'Invalid VLESS URL: missing port' }; + return { valid: false, message: _('Invalid VLESS URL: missing port') }; } if ( @@ -38,15 +37,16 @@ export function validateVlessUrl(url: string): ValidationResult { ) { return { valid: false, - message: + message: _( 'Invalid VLESS URL: invalid port number. Must be between 1 and 65535', + ), }; } if (!parsedUrl.search) { return { valid: false, - message: 'Invalid VLESS URL: missing query parameters', + message: _('Invalid VLESS URL: missing query parameters'), }; } @@ -68,8 +68,9 @@ export function validateVlessUrl(url: string): ValidationResult { if (!type || !validTypes.includes(type)) { return { valid: false, - message: + message: _( 'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws', + ), }; } @@ -79,8 +80,9 @@ export function validateVlessUrl(url: string): ValidationResult { if (!security || !validSecurities.includes(security)) { return { valid: false, - message: + message: _( 'Invalid VLESS URL: security must be one of tls, reality, none', + ), }; } @@ -88,21 +90,23 @@ export function validateVlessUrl(url: string): ValidationResult { if (!params.get('pbk')) { return { valid: false, - message: + message: _( 'Invalid VLESS URL: missing pbk parameter for reality security', + ), }; } if (!params.get('fp')) { return { valid: false, - message: + message: _( 'Invalid VLESS URL: missing fp parameter for reality security', + ), }; } } - return { valid: true, message: 'Valid' }; + return { valid: true, message: _('Valid') }; } catch (_e) { - return { valid: false, message: 'Invalid VLESS URL: parsing failed' }; + return { valid: false, message: _('Invalid VLESS URL: parsing failed') }; } } diff --git a/fe-app-podkop/tests/setup/global-mocks.ts b/fe-app-podkop/tests/setup/global-mocks.ts new file mode 100644 index 0000000..8f93270 --- /dev/null +++ b/fe-app-podkop/tests/setup/global-mocks.ts @@ -0,0 +1,2 @@ +// tests/setup/global-mocks.ts +globalThis._ = (key: string) => key; diff --git a/fe-app-podkop/vitest.config.js b/fe-app-podkop/vitest.config.js index adbf725..a8eb868 100644 --- a/fe-app-podkop/vitest.config.js +++ b/fe-app-podkop/vitest.config.js @@ -4,5 +4,6 @@ export default defineConfig({ test: { globals: true, environment: 'node', + setupFiles: ['./tests/setup/global-mocks.ts'], }, }); diff --git a/fe-app-podkop/watch-upload.js b/fe-app-podkop/watch-upload.js new file mode 100644 index 0000000..db0b1ea --- /dev/null +++ b/fe-app-podkop/watch-upload.js @@ -0,0 +1,82 @@ +import 'dotenv/config'; +import chokidar from 'chokidar'; +import SFTPClient from 'ssh2-sftp-client'; +import path from 'path'; +import fs from 'fs'; +import { glob } from 'glob'; + +const sftp = new SFTPClient(); + +const config = { + host: process.env.SFTP_HOST, + port: Number(process.env.SFTP_PORT || 22), + username: process.env.SFTP_USER, + ...(process.env.SFTP_PRIVATE_KEY + ? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) } + : { password: process.env.SFTP_PASS }), +}; + +const localDir = path.resolve(process.env.LOCAL_DIR || './dist'); +const remoteDir = process.env.REMOTE_DIR || '/www/luci-static/mypkg'; + +async function uploadFile(filePath) { + const relativePath = path.relative(localDir, filePath); + const remotePath = path.posix.join(remoteDir, relativePath); + + console.log(`Uploading: ${relativePath} -> ${remotePath}`); + try { + await sftp.fastPut(filePath, remotePath); + console.log(`Uploaded: ${relativePath}`); + } catch (err) { + console.error(`Failed: ${relativePath}: ${err.message}`); + } +} + +async function deleteFile(filePath) { + const relativePath = path.relative(localDir, filePath); + const remotePath = path.posix.join(remoteDir, relativePath); + + console.log(`Removing: ${relativePath}`); + try { + await sftp.delete(remotePath); + console.log(`Removed: ${relativePath}`); + } catch (err) { + 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); + } + + console.log('Initial upload complete!'); +} + +async function main() { + await sftp.connect(config); + 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); + } + }); + + process.on('SIGINT', async () => { + console.log('Disconnecting...'); + await sftp.end(); + process.exit(); + }); +} + +main().catch(console.error); diff --git a/fe-app-podkop/yarn.lock b/fe-app-podkop/yarn.lock index 6791013..93738ea 100644 --- a/fe-app-podkop/yarn.lock +++ b/fe-app-podkop/yarn.lock @@ -221,6 +221,18 @@ resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -628,6 +640,13 @@ argparse@^2.0.1: resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -638,6 +657,13 @@ balanced-match@^1.0.0: resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + brace-expansion@^1.1.7: version "1.1.12" resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" @@ -660,6 +686,16 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + bundle-require@^5.1.0: version "5.1.0" resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" @@ -701,7 +737,7 @@ check-error@^2.1.1: resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== -chokidar@^4.0.3: +chokidar@4.0.3, chokidar@^4.0.3: version "4.0.3" resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== @@ -730,6 +766,16 @@ concat-map@0.0.1: resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + confbox@^0.1.8: version "0.1.8" resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" @@ -740,6 +786,14 @@ consola@^3.4.0: resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -766,6 +820,11 @@ deep-is@^0.1.3: resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +dotenv@17.2.3: + version "17.2.3" + resolved "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" + integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -1014,7 +1073,7 @@ flatted@^3.2.9: resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== -foreground-child@^3.1.0: +foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -1041,6 +1100,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@11.0.3: + version "11.0.3" + resolved "https://registry.npmmirror.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" + integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== + dependencies: + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.0.3" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^10.3.10: version "10.4.5" resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" @@ -1091,6 +1162,11 @@ imurmurhash@^0.1.4: resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1127,6 +1203,13 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + joycon@^3.1.1: version "3.1.1" resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" @@ -1216,6 +1299,11 @@ lru-cache@^10.2.0: resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.0.0: + version "11.2.2" + resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24" + integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg== + magic-string@^0.30.17: version "0.30.19" resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" @@ -1236,6 +1324,13 @@ micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1279,6 +1374,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@^2.19.0, nan@^2.23.0: + version "2.23.0" + resolved "https://registry.npmmirror.com/nan/-/nan-2.23.0.tgz#24aa4ddffcc37613a2d2935b97683c1ec96093c6" + integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ== + nanoid@^3.3.11: version "3.3.11" resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -1350,6 +1450,14 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + pathe@^2.0.1, pathe@^2.0.3: version "2.0.3" resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" @@ -1425,6 +1533,15 @@ queue-microtask@^1.2.2: resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@^4.0.1: version "4.1.2" resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" @@ -1483,6 +1600,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^7.6.0: version "7.7.2" resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" @@ -1522,6 +1649,25 @@ source-map@0.8.0-beta.0: dependencies: whatwg-url "^7.0.0" +ssh2-sftp-client@12.0.1: + version "12.0.1" + resolved "https://registry.npmmirror.com/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz#926764878954dbed85f6f9233ce7980bfc94fdd4" + integrity sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg== + dependencies: + concat-stream "^2.0.0" + ssh2 "^1.16.0" + +ssh2@^1.16.0: + version "1.17.0" + resolved "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz#dc686e8e3abdbd4ad95d46fa139615903c12258c" + integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.23.0" + stackback@0.0.2: version "0.0.2" resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -1559,6 +1705,13 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -1711,6 +1864,11 @@ tsup@8.5.0: tinyglobby "^0.2.11" tree-kill "^1.2.2" +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -1718,6 +1876,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typescript-eslint@8.45.0: version "8.45.0" resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b" @@ -1745,6 +1908,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + vite-node@3.2.4: version "3.2.4" resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" 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 index 2686fb0..d317048 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js @@ -88,7 +88,7 @@ function createAdditionalSection(mainSection) { return true; } - return _(validation.message); + return validation.message; }; o = mainSection.taboption( @@ -113,7 +113,7 @@ function createAdditionalSection(mainSection) { return true; } - return _(validation.message); + return validation.message; }; o = mainSection.taboption( @@ -342,7 +342,7 @@ function createAdditionalSection(mainSection) { return true; } - return _(validation.message); + return validation.message; }; o = mainSection.taboption( 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 index 6ce0f09..b4f3b17 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -12,11 +12,11 @@ function createConfigSection(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'), + 'basic', + form.ListValue, + 'mode', + _('Connection Type'), + _('Select between VPN and Proxy connection methods for traffic routing'), ); o.value('proxy', 'Proxy'); o.value('vpn', 'VPN'); @@ -24,11 +24,11 @@ function createConfigSection(section) { o.ucisection = s.section; o = s.taboption( - 'basic', - form.ListValue, - 'proxy_config_type', - _('Configuration Type'), - _('Select how to configure the proxy'), + 'basic', + form.ListValue, + 'proxy_config_type', + _('Configuration Type'), + _('Select how to configure the proxy'), ); o.value('url', _('Connection URL')); o.value('outbound', _('Outbound Config')); @@ -38,11 +38,11 @@ function createConfigSection(section) { o.ucisection = s.section; o = s.taboption( - 'basic', - form.TextValue, - 'proxy_string', - _('Proxy Configuration URL'), - '', + 'basic', + form.TextValue, + 'proxy_string', + _('Proxy Configuration URL'), + '', ); o.depends('proxy_config_type', 'url'); o.rows = 5; @@ -52,7 +52,7 @@ function createConfigSection(section) { 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'; + '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, [ @@ -66,9 +66,9 @@ function createConfigSection(section) { if (cfgvalue) { try { const activeConfig = cfgvalue - .split('\n') - .map((line) => line.trim()) - .find((line) => line && !line.startsWith('//')); + .split('\n') + .map((line) => line.trim()) + .find((line) => line && !line.startsWith('//')); if (activeConfig) { if (activeConfig.includes('#')) { @@ -76,24 +76,24 @@ function createConfigSection(section) { if (label && label.trim()) { const decodedLabel = decodeURIComponent(label); const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Current config: ') + decodedLabel, + 'div', + { class: 'cbi-value-description' }, + _('Current config: ') + decodedLabel, ); container.appendChild(descDiv); } else { const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), + 'div', + { class: 'cbi-value-description' }, + _('Config without description'), ); container.appendChild(descDiv); } } else { const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), + 'div', + { class: 'cbi-value-description' }, + _('Config without description'), ); container.appendChild(descDiv); } @@ -101,19 +101,19 @@ function createConfigSection(section) { } catch (e) { console.error('Error parsing config label:', e); const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), + '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', - ), + '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); } @@ -129,20 +129,20 @@ function createConfigSection(section) { try { const activeConfigs = value - .split('\n') - .map((line) => line.trim()) - .filter((line) => !line.startsWith('//')) - .filter(Boolean); + .split('\n') + .map((line) => line.trim()) + .filter((line) => !line.startsWith('//')) + .filter(Boolean); if (!activeConfigs.length) { return _( - 'No active configuration found. One configuration is required.', + 'No active configuration found. One configuration is required.', ); } if (activeConfigs.length > 1) { return _( - 'Multiply active configurations found. Please leave one configuration.', + 'Multiply active configurations found. Please leave one configuration.', ); } @@ -152,18 +152,18 @@ function createConfigSection(section) { return true; } - return _(validation.message); + 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'), + 'basic', + form.TextValue, + 'outbound_json', + _('Outbound Configuration'), + _('Enter complete outbound configuration in JSON format'), ); o.depends('proxy_config_type', 'outbound'); o.rows = 10; @@ -180,14 +180,14 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.DynamicList, - 'urltest_proxy_links', - _('URLTest Proxy Links'), + 'basic', + form.DynamicList, + 'urltest_proxy_links', + _('URLTest Proxy Links'), ); o.depends('proxy_config_type', 'urltest'); o.placeholder = 'vless://, ss://, trojan:// links'; @@ -204,15 +204,15 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.Flag, - 'ss_uot', - _('Shadowsocks UDP over TCP'), - _('Apply for SS2022'), + 'basic', + form.Flag, + 'ss_uot', + _('Shadowsocks UDP over TCP'), + _('Apply for SS2022'), ); o.default = '0'; o.depends('mode', 'proxy'); @@ -220,11 +220,11 @@ function createConfigSection(section) { o.ucisection = s.section; o = s.taboption( - 'basic', - widgets.DeviceSelect, - 'interface', - _('Network Interface'), - _('Select network interface for VPN connection'), + 'basic', + widgets.DeviceSelect, + 'interface', + _('Network Interface'), + _('Select network interface for VPN connection'), ); o.depends('mode', 'vpn'); o.ucisection = s.section; @@ -262,17 +262,17 @@ function createConfigSection(section) { // Reject wireless-related devices const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); + 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'), + '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; @@ -280,11 +280,11 @@ function createConfigSection(section) { 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'), + '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)')); @@ -295,11 +295,11 @@ function createConfigSection(section) { o.ucisection = s.section; o = s.taboption( - 'basic', - form.Value, - 'domain_resolver_dns_server', - _('DNS Server'), - _('Select or enter DNS server address'), + '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)); @@ -315,25 +315,25 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.Flag, - 'community_lists_enabled', - _('Community Lists'), + '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') + + 'basic', + form.DynamicList, + 'community_lists', + _('Service List'), + _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains', ); o.placeholder = 'Service list'; @@ -357,50 +357,50 @@ function createConfigSection(section) { let notifications = []; const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => - newValues.includes(opt), + newValues.includes(opt), ); if (selectedRegionalOptions.length > 1) { const lastSelected = - selectedRegionalOptions[selectedRegionalOptions.length - 1]; + selectedRegionalOptions[selectedRegionalOptions.length - 1]; const removedRegions = selectedRegionalOptions.slice(0, -1); newValues = newValues.filter( - (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), + (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), - ]), + 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), + (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), ); if (removedServices.length > 0) { newValues = newValues.filter((v) => - main.ALLOWED_WITH_RUSSIA_INSIDE.includes(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(', '), - ), - ]), + 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(', '), + ), + ]), ); } } @@ -410,7 +410,7 @@ function createConfigSection(section) { } notifications.forEach((notification) => - ui.addNotification(null, notification), + ui.addNotification(null, notification), ); lastValues = newValues; } catch (e) { @@ -421,11 +421,11 @@ function createConfigSection(section) { }; o = s.taboption( - 'basic', - form.ListValue, - 'user_domain_list_type', - _('User Domain List Type'), - _('Select how to add your custom domains'), + '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')); @@ -435,13 +435,13 @@ function createConfigSection(section) { 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)', - ), + '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'); @@ -459,20 +459,20 @@ function createConfigSection(section) { return true; } - return _(validation.message); + 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 //', - ), + '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'; + '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; @@ -487,7 +487,7 @@ function createConfigSection(section) { if (!domains.length) { return _( - 'At least one valid domain must be specified. Comments-only content is not allowed.', + 'At least one valid domain must be specified. Comments-only content is not allowed.', ); } @@ -495,8 +495,8 @@ function createConfigSection(section) { if (!valid) { const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors return [_('Validation errors:'), ...errors].join('\n'); } @@ -505,22 +505,22 @@ function createConfigSection(section) { }; o = s.taboption( - 'basic', - form.Flag, - 'local_domain_lists_enabled', - _('Local Domain Lists'), - _('Use the list from the router filesystem'), + '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'), + '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'); @@ -538,26 +538,26 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.Flag, - 'remote_domain_lists_enabled', - _('Remote Domain Lists'), - _('Download and use domain lists from remote URLs'), + '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://'), + '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'); @@ -575,26 +575,26 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.Flag, - 'local_subnet_lists_enabled', - _('Local Subnet Lists'), - _('Use the list from the router filesystem'), + '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'), + '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'); @@ -612,15 +612,15 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; o = s.taboption( - 'basic', - form.ListValue, - 'user_subnet_list_type', - _('User Subnet List Type'), - _('Select how to add your custom subnets'), + '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')); @@ -630,13 +630,13 @@ function createConfigSection(section) { 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', - ), + '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'); @@ -654,20 +654,20 @@ function createConfigSection(section) { return true; } - return _(validation.message); + 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 //', - ), + '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'; + '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; @@ -682,7 +682,7 @@ function createConfigSection(section) { if (!subnets.length) { return _( - 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', + 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', ); } @@ -690,8 +690,8 @@ function createConfigSection(section) { if (!valid) { const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors return [_('Validation errors:'), ...errors].join('\n'); } @@ -700,22 +700,22 @@ function createConfigSection(section) { }; o = s.taboption( - 'basic', - form.Flag, - 'remote_subnet_lists_enabled', - _('Remote Subnet Lists'), - _('Download and use subnet lists from remote URLs'), + '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://'), + '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'); @@ -733,28 +733,28 @@ function createConfigSection(section) { return true; } - return _(validation.message); + 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', - ), + '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'), + 'basic', + form.DynamicList, + 'all_traffic_ip', + _('Local IPs'), + _('Enter valid IPv4 addresses'), ); o.placeholder = 'IP'; o.depends('all_traffic_from_ip_enabled', '1'); @@ -772,7 +772,7 @@ function createConfigSection(section) { return true; } - return _(validation.message); + return validation.message; }; } 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 new file mode 100644 index 0000000..a5056dc --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js @@ -0,0 +1,26 @@ +'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/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index ec52014..34cde9e 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 @@ -2,45 +2,48 @@ "use strict"; "require baseclass"; "require fs"; +"require uci"; // src/validators/validateIp.ts function validateIPV4(ip) { const ipRegex = /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; if (ipRegex.test(ip)) { - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } - return { valid: false, message: "Invalid IP address" }; + return { valid: false, message: _("Invalid IP address") }; } // src/validators/validateDomain.ts function validateDomain(domain) { const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; if (!domainRegex.test(domain)) { - return { valid: false, message: "Invalid domain address" }; + return { valid: false, message: _("Invalid domain address") }; } const hostname = domain.split("/")[0]; const parts = hostname.split("."); const atLeastOneInvalidPart = parts.some((part) => part.length > 63); if (atLeastOneInvalidPart) { - return { valid: false, message: "Invalid domain address" }; + return { valid: false, message: _("Invalid domain address") }; } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } // src/validators/validateDns.ts function validateDNS(value) { if (!value) { - return { valid: false, message: "DNS server address cannot be empty" }; + return { valid: false, message: _("DNS server address cannot be empty") }; } if (validateIPV4(value).valid) { - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } if (validateDomain(value).valid) { - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } return { valid: false, - message: "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" + message: _( + "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" + ) }; } @@ -51,12 +54,12 @@ function validateUrl(url, protocols = ["http:", "https:"]) { if (!protocols.includes(parsedUrl.protocol)) { return { valid: false, - message: `URL must use one of the following protocols: ${protocols.join(", ")}` + message: `${_("URL must use one of the following protocols:")} ${protocols.join(", ")}` }; } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } catch (_e) { - return { valid: false, message: "Invalid URL format" }; + return { valid: false, message: _("Invalid URL format") }; } } @@ -65,7 +68,7 @@ function validatePath(value) { if (!value) { return { valid: false, - message: "Path cannot be empty" + message: _("Path cannot be empty") }; } const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/; @@ -77,7 +80,9 @@ function validatePath(value) { } return { valid: false, - message: 'Invalid path format. Path must start with "/" and contain valid characters' + message: _( + 'Invalid path format. Path must start with "/" and contain valid characters' + ) }; } @@ -87,12 +92,12 @@ function validateSubnet(value) { if (!subnetRegex.test(value)) { return { valid: false, - message: "Invalid format. Use X.X.X.X or X.X.X.X/Y" + message: _("Invalid format. Use X.X.X.X or X.X.X.X/Y") }; } const [ip, cidr] = value.split("/"); if (ip === "0.0.0.0") { - return { valid: false, message: "IP address 0.0.0.0 is not allowed" }; + return { valid: false, message: _("IP address 0.0.0.0 is not allowed") }; } const ipCheck = validateIPV4(ip); if (!ipCheck.valid) { @@ -103,11 +108,11 @@ function validateSubnet(value) { if (cidrNum < 0 || cidrNum > 32) { return { valid: false, - message: "CIDR must be between 0 and 32" + message: _("CIDR must be between 0 and 32") }; } } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } // src/validators/bulkValidate.ts @@ -124,14 +129,14 @@ function validateShadowsocksUrl(url) { if (!url.startsWith("ss://")) { return { valid: false, - message: "Invalid Shadowsocks URL: must start with ss://" + message: _("Invalid Shadowsocks URL: must start with ss://") }; } try { if (!url || /\s/.test(url)) { return { valid: false, - message: "Invalid Shadowsocks URL: must not contain spaces" + message: _("Invalid Shadowsocks URL: must not contain spaces") }; } const mainPart = url.includes("?") ? url.split("?")[0] : url.split("#")[0]; @@ -139,7 +144,7 @@ function validateShadowsocksUrl(url) { if (!encryptedPart) { return { valid: false, - message: "Invalid Shadowsocks URL: missing credentials" + message: _("Invalid Shadowsocks URL: missing credentials") }; } try { @@ -147,14 +152,18 @@ function validateShadowsocksUrl(url) { if (!decoded.includes(":")) { return { valid: false, - message: "Invalid Shadowsocks URL: decoded credentials must contain method:password" + message: _( + "Invalid Shadowsocks URL: decoded credentials must contain method:password" + ) }; } } catch (_e) { if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) { return { valid: false, - message: 'Invalid Shadowsocks URL: missing method and password separator ":"' + message: _( + 'Invalid Shadowsocks URL: missing method and password separator ":"' + ) }; } } @@ -162,31 +171,37 @@ function validateShadowsocksUrl(url) { if (!serverPart) { return { valid: false, - message: "Invalid Shadowsocks URL: missing server address" + message: _("Invalid Shadowsocks URL: missing server address") }; } const [server, portAndRest] = serverPart.split(":"); if (!server) { return { valid: false, - message: "Invalid Shadowsocks URL: missing server" + message: _("Invalid Shadowsocks URL: missing server") }; } const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; if (!port) { - return { valid: false, message: "Invalid Shadowsocks URL: missing port" }; + return { + valid: false, + message: _("Invalid Shadowsocks URL: missing port") + }; } const portNum = parseInt(port, 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { return { valid: false, - message: "Invalid port number. Must be between 1 and 65535" + message: _("Invalid port number. Must be between 1 and 65535") }; } } catch (_e) { - return { valid: false, message: "Invalid Shadowsocks URL: parsing failed" }; + return { + valid: false, + message: _("Invalid Shadowsocks URL: parsing failed") + }; } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } // src/validators/validateVlessUrl.ts @@ -196,34 +211,36 @@ function validateVlessUrl(url) { if (!url || /\s/.test(url)) { return { valid: false, - message: "Invalid VLESS URL: must not contain spaces" + message: _("Invalid VLESS URL: must not contain spaces") }; } if (parsedUrl.protocol !== "vless:") { return { valid: false, - message: "Invalid VLESS URL: must start with vless://" + message: _("Invalid VLESS URL: must start with vless://") }; } if (!parsedUrl.username) { - return { valid: false, message: "Invalid VLESS URL: missing UUID" }; + return { valid: false, message: _("Invalid VLESS URL: missing UUID") }; } if (!parsedUrl.hostname) { - return { valid: false, message: "Invalid VLESS URL: missing server" }; + return { valid: false, message: _("Invalid VLESS URL: missing server") }; } if (!parsedUrl.port) { - return { valid: false, message: "Invalid VLESS URL: missing port" }; + return { valid: false, message: _("Invalid VLESS URL: missing port") }; } if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) { return { valid: false, - message: "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" + message: _( + "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" + ) }; } if (!parsedUrl.search) { return { valid: false, - message: "Invalid VLESS URL: missing query parameters" + message: _("Invalid VLESS URL: missing query parameters") }; } const params = new URLSearchParams(parsedUrl.search); @@ -242,7 +259,9 @@ function validateVlessUrl(url) { if (!type || !validTypes.includes(type)) { return { valid: false, - message: "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" + message: _( + "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" + ) }; } const security = params.get("security"); @@ -250,26 +269,32 @@ function validateVlessUrl(url) { if (!security || !validSecurities.includes(security)) { return { valid: false, - message: "Invalid VLESS URL: security must be one of tls, reality, none" + message: _( + "Invalid VLESS URL: security must be one of tls, reality, none" + ) }; } if (security === "reality") { if (!params.get("pbk")) { return { valid: false, - message: "Invalid VLESS URL: missing pbk parameter for reality security" + message: _( + "Invalid VLESS URL: missing pbk parameter for reality security" + ) }; } if (!params.get("fp")) { return { valid: false, - message: "Invalid VLESS URL: missing fp parameter for reality security" + message: _( + "Invalid VLESS URL: missing fp parameter for reality security" + ) }; } } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } catch (_e) { - return { valid: false, message: "Invalid VLESS URL: parsing failed" }; + return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; } } @@ -280,12 +305,14 @@ function validateOutboundJson(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' + message: _( + 'Outbound JSON must contain at least "type", "server" and "server_port" fields' + ) }; } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } catch { - return { valid: false, message: "Invalid JSON format" }; + return { valid: false, message: _("Invalid JSON format") }; } } @@ -294,13 +321,13 @@ function validateTrojanUrl(url) { if (!url.startsWith("trojan://")) { return { valid: false, - message: "Invalid Trojan URL: must start with trojan://" + message: _("Invalid Trojan URL: must start with trojan://") }; } if (!url || /\s/.test(url)) { return { valid: false, - message: "Invalid Trojan URL: must not contain spaces" + message: _("Invalid Trojan URL: must not contain spaces") }; } try { @@ -308,13 +335,15 @@ function validateTrojanUrl(url) { if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { return { valid: false, - message: "Invalid Trojan URL: must contain username, hostname and port" + message: _( + "Invalid Trojan URL: must contain username, hostname and port" + ) }; } } catch (_e) { - return { valid: false, message: "Invalid Trojan URL: parsing failed" }; + return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; } - return { valid: true, message: "Valid" }; + return { valid: true, message: _("Valid") }; } // src/validators/validateProxyUrl.ts @@ -330,7 +359,7 @@ function validateProxyUrl(url) { } return { valid: false, - message: "URL must start with vless:// or ss:// or trojan://" + message: _("URL must start with vless:// or ss:// or trojan://") }; } @@ -370,6 +399,157 @@ var GlobalStyles = ` #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 @@ -385,10 +565,10 @@ function injectGlobalStyles() { } // src/helpers/withTimeout.ts -async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = "Operation timed out") { +async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = _("Operation timed out")) { let timeoutId; const start = performance.now(); - const timeoutPromise = new Promise((_, reject) => { + const timeoutPromise = new Promise((_2, reject) => { timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); }); try { @@ -528,34 +708,1147 @@ async function executeShellCommand({ } } -// 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"); - return { - success: true, - message: "Copied!" - }; - } catch (err) { - const error = err; - return { - success: false, - message: `Failed to copy: ${error.message}` - }; - } finally { - document.body.removeChild(textarea); - } -} - // 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 { protocol, hostname } = window.location; + return `${protocol}//${hostname}:9090`; +} +function getClashWsUrl() { + const { hostname } = window.location; + return `ws://${hostname}:9090`; +} + +// src/clash/methods/createBaseApiRequest.ts +async function createBaseApiRequest(fetchFn) { + try { + const response = await fetchFn(); + if (!response.ok) { + return { + success: false, + message: `${_("HTTP error")} ${response.status}: ${response.statusText}` + }; + } + const data = await response.json(); + return { + success: true, + data + }; + } catch (e) { + return { + success: false, + message: e instanceof Error ? e.message : _("Unknown error") + }; + } +} + +// src/clash/methods/getConfig.ts +async function getClashConfig() { + return createBaseApiRequest( + () => fetch(`${getClashApiUrl()}/configs`, { + 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` + ); + return { + withTagSelect: false, + code: outbound?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || section[".name"], + displayName: getProxyUrlName(section.proxy_string) || outbound?.value?.name || "", + 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` + ); + return { + withTagSelect: false, + code: outbound?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || section[".name"], + displayName: decodeURIComponent(JSON.parse(section.outbound_json)?.tag) || outbound?.value?.name || "", + 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.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 + } + ] + }; + } + 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: 1e3 + }); + 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: 1e3 + }); + if (response.stdout) { + return JSON.parse(response.stdout.replace(/\n/g, "")); + } + return { running: 0, enabled: 0, status: "unknown" }; +} + +// src/podkop/services/tab.service.ts +var TabService = class _TabService { + constructor() { + this.observer = null; + this.lastActiveId = null; + this.init(); + } + static getInstance() { + if (!_TabService.instance) { + _TabService.instance = new _TabService(); + } + return _TabService.instance; + } + init() { + this.observer = new MutationObserver(() => this.handleMutations()); + this.observer.observe(document.body, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ["class"] + }); + this.notify(); + } + handleMutations() { + this.notify(); + } + getTabsInfo() { + const tabs = Array.from( + document.querySelectorAll(".cbi-tab, .cbi-tab-disabled") + ); + return tabs.map((el) => ({ + el, + id: el.dataset.tab || "", + active: el.classList.contains("cbi-tab") && !el.classList.contains("cbi-tab-disabled") + })); + } + getActiveTabId() { + const active = document.querySelector( + ".cbi-tab:not(.cbi-tab-disabled)" + ); + return active?.dataset.tab || null; + } + notify() { + const tabs = this.getTabsInfo(); + const activeId = this.getActiveTabId(); + if (activeId !== this.lastActiveId) { + this.lastActiveId = activeId; + this.callback?.(activeId, tabs); + } + } + onChange(callback) { + this.callback = callback; + this.notify(); + } + getAllTabs() { + return this.getTabsInfo(); + } + getActiveTab() { + return this.getActiveTabId(); + } + disconnect() { + this.observer?.disconnect(); + this.observer = null; + } +}; +var TabServiceInstance = TabService.getInstance(); + +// src/store.ts +function jsonStableStringify(obj) { + return JSON.stringify(obj, (_2, value) => { + if (value && typeof value === "object" && !Array.isArray(value)) { + return Object.keys(value).sort().reduce( + (acc, key) => { + acc[key] = value[key]; + return acc; + }, + {} + ); + } + return value; + }); +} +function jsonEqual(a, b) { + try { + return jsonStableStringify(a) === jsonStableStringify(b); + } catch { + return false; + } +} +var Store = class { + constructor(initial) { + this.listeners = /* @__PURE__ */ new Set(); + this.lastHash = ""; + this.value = initial; + this.initial = structuredClone(initial); + this.lastHash = jsonStableStringify(initial); + } + get() { + return this.value; + } + set(next) { + const prev = this.value; + const merged = { ...prev, ...next }; + if (jsonEqual(prev, merged)) return; + this.value = merged; + this.lastHash = jsonStableStringify(merged); + const diff = {}; + for (const key in merged) { + if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key]; + } + this.listeners.forEach((cb) => cb(this.value, prev, diff)); + } + reset() { + const prev = this.value; + const next = structuredClone(this.initial); + if (jsonEqual(prev, next)) return; + this.value = next; + this.lastHash = jsonStableStringify(next); + const diff = {}; + for (const key in next) { + if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; + } + this.listeners.forEach((cb) => cb(this.value, prev, diff)); + } + subscribe(cb) { + this.listeners.add(cb); + cb(this.value, this.value, {}); + return () => this.listeners.delete(cb); + } + unsubscribe(cb) { + this.listeners.delete(cb); + } + patch(key, value) { + this.set({ [key]: value }); + } + getKey(key) { + return this.value[key]; + } + subscribeKey(key, cb) { + let prev = this.value[key]; + const wrapper = (val) => { + if (!jsonEqual(val[key], prev)) { + prev = val[key]; + cb(val[key]); + } + }; + this.listeners.add(wrapper); + return () => this.listeners.delete(wrapper); + } +}; +var initialStore = { + tabService: { + current: "", + all: [] + }, + bandwidthWidget: { + loading: true, + failed: false, + data: { up: 0, down: 0 } + }, + trafficTotalWidget: { + loading: true, + failed: false, + data: { downloadTotal: 0, uploadTotal: 0 } + }, + systemInfoWidget: { + loading: true, + failed: false, + data: { connections: 0, memory: 0 } + }, + servicesInfoWidget: { + loading: true, + failed: false, + data: { singbox: 0, podkop: 0 } + }, + sectionsWidget: { + loading: true, + failed: false, + data: [] + } +}; +var store = new Store(initialStore); + +// src/podkop/services/core.service.ts +function coreService() { + TabServiceInstance.onChange((activeId, tabs) => { + store.set({ + tabService: { + current: activeId || "", + all: tabs.map((tab) => tab.id) + } + }); + }); +} + +// src/podkop/tabs/dashboard/renderSections.ts +function renderFailedState() { + return E( + "div", + { + class: "pdk_dashboard-page__outbound-section centered", + style: "height: 127px" + }, + E("span", {}, _("Dashboard currently unavailable")) + ); +} +function renderLoadingState() { + return E("div", { + id: "dashboard-sections-grid-skeleton", + class: "pdk_dashboard-page__outbound-section skeleton", + style: "height: 127px" + }); +} +function renderDefaultState({ + section, + onChooseOutbound, + onTestLatency +}) { + function testLatency() { + if (section.withTagSelect) { + return onTestLatency(section.code); + } + if (section.outbounds.length) { + return onTestLatency(section.outbounds[0].code); + } + } + function renderOutbound(outbound) { + function getLatencyClass() { + if (!outbound.latency) { + return "pdk_dashboard-page__outbound-grid__item__latency--empty"; + } + if (outbound.latency < 200) { + return "pdk_dashboard-page__outbound-grid__item__latency--green"; + } + if (outbound.latency < 400) { + return "pdk_dashboard-page__outbound-grid__item__latency--yellow"; + } + return "pdk_dashboard-page__outbound-grid__item__latency--red"; + } + return E( + "div", + { + class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""} ${section.withTagSelect ? "pdk_dashboard-page__outbound-grid__item--selectable" : ""}`, + click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code) + }, + [ + E("b", {}, outbound.displayName), + E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [ + E( + "div", + { class: "pdk_dashboard-page__outbound-grid__item__type" }, + outbound.type + ), + E( + "div", + { class: getLatencyClass() }, + outbound.latency ? `${outbound.latency}ms` : "N/A" + ) + ]) + ] + ); + } + return E("div", { class: "pdk_dashboard-page__outbound-section" }, [ + // Title with test latency + E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [ + E( + "div", + { + class: "pdk_dashboard-page__outbound-section__title-section__title" + }, + section.displayName + ), + E( + "button", + { + class: "btn dashboard-sections-grid-item-test-latency", + click: () => testLatency() + }, + "Test latency" + ) + ]), + E( + "div", + { class: "pdk_dashboard-page__outbound-grid" }, + section.outbounds.map((outbound) => renderOutbound(outbound)) + ) + ]); +} +function renderSections(props) { + if (props.failed) { + return renderFailedState(); + } + if (props.loading) { + return renderLoadingState(); + } + return renderDefaultState(props); +} + +// src/podkop/tabs/dashboard/renderWidget.ts +function renderFailedState2() { + return E( + "div", + { + id: "", + style: "height: 78px", + class: "pdk_dashboard-page__widgets-section__item centered" + }, + _("Currently unavailable") + ); +} +function renderLoadingState2() { + return E( + "div", + { + id: "", + style: "height: 78px", + class: "pdk_dashboard-page__widgets-section__item skeleton" + }, + "" + ); +} +function renderDefaultState2({ title, items }) { + return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [ + E( + "b", + { class: "pdk_dashboard-page__widgets-section__item__title" }, + title + ), + ...items.map( + (item) => E( + "div", + { + class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` + }, + [ + E( + "span", + { class: "pdk_dashboard-page__widgets-section__item__row__key" }, + `${item.key}: ` + ), + E( + "span", + { class: "pdk_dashboard-page__widgets-section__item__row__value" }, + item.value + ) + ] + ) + ) + ]); +} +function renderWidget(props) { + if (props.loading) { + return renderLoadingState2(); + } + if (props.failed) { + return renderFailedState2(); + } + return renderDefaultState2(props); +} + +// src/podkop/tabs/dashboard/renderDashboard.ts +function renderDashboard() { + return E( + "div", + { + id: "dashboard-status", + class: "pdk_dashboard-page" + }, + [ + // Widgets section + E("div", { class: "pdk_dashboard-page__widgets-section" }, [ + E( + "div", + { id: "dashboard-widget-traffic" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-traffic-total" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-system-info" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-service-info" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ) + ]), + // All outbounds + E( + "div", + { id: "dashboard-sections-grid" }, + renderSections({ + loading: true, + failed: false, + section: { + code: "", + displayName: "", + outbounds: [], + withTagSelect: false + }, + onTestLatency: () => { + }, + onChooseOutbound: () => { + } + }) + ) + ] + ); +} + +// 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"]; + if (n < 1e3) { + return n + " B"; + } + const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); + n = Number((n / Math.pow(1e3, exponent)).toPrecision(3)); + const unit = UNITS[exponent]; + return n + " " + unit; +} + +// src/podkop/tabs/dashboard/initDashboardController.ts +async function fetchDashboardSections() { + const prev = store.get().sectionsWidget; + store.set({ + sectionsWidget: { + ...prev, + failed: false + } + }); + const { data, success } = await getDashboardSections(); + store.set({ + sectionsWidget: { + loading: false, + failed: !success, + data + } + }); +} +async function fetchServicesInfo() { + const [podkop, singbox] = await Promise.all([ + getPodkopStatus(), + getSingboxStatus() + ]); + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.running, podkop: podkop.enabled } + } + }); +} +async function connectToClashSockets() { + socket.subscribe( + `${getClashWsUrl()}/traffic?token=`, + (msg) => { + const parsedMsg = JSON.parse(msg); + store.set({ + bandwidthWidget: { + loading: false, + failed: false, + data: { up: parsedMsg.up, down: parsedMsg.down } + } + }); + }, + (_err) => { + store.set({ + bandwidthWidget: { + loading: false, + failed: true, + data: { up: 0, down: 0 } + } + }); + } + ); + socket.subscribe( + `${getClashWsUrl()}/connections?token=`, + (msg) => { + const parsedMsg = JSON.parse(msg); + store.set({ + trafficTotalWidget: { + loading: false, + failed: false, + data: { + downloadTotal: parsedMsg.downloadTotal, + uploadTotal: parsedMsg.uploadTotal + } + }, + systemInfoWidget: { + loading: false, + failed: false, + data: { + connections: parsedMsg.connections?.length, + memory: parsedMsg.memory + } + } + }); + }, + (_err) => { + store.set({ + trafficTotalWidget: { + loading: false, + failed: true, + data: { downloadTotal: 0, uploadTotal: 0 } + }, + systemInfoWidget: { + loading: false, + failed: true, + data: { + connections: 0, + memory: 0 + } + } + }); + } + ); +} +async function handleChooseOutbound(selector, tag) { + await triggerProxySelector(selector, tag); + await fetchDashboardSections(); +} +async function handleTestGroupLatency(tag) { + await triggerLatencyGroupTest(tag); + await fetchDashboardSections(); +} +async function handleTestProxyLatency(tag) { + await triggerLatencyProxyTest(tag); + await fetchDashboardSections(); +} +function replaceTestLatencyButtonsWithSkeleton() { + document.querySelectorAll(".dashboard-sections-grid-item-test-latency").forEach((el) => { + const newDiv = document.createElement("div"); + newDiv.className = "skeleton"; + newDiv.style.width = "99px"; + newDiv.style.height = "28px"; + el.replaceWith(newDiv); + }); +} +async function renderSectionsWidget() { + console.log("renderSectionsWidget"); + const sectionsWidget = store.get().sectionsWidget; + const container = document.getElementById("dashboard-sections-grid"); + if (sectionsWidget.loading || sectionsWidget.failed) { + const renderedWidget = renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section: { + code: "", + displayName: "", + outbounds: [], + withTagSelect: false + }, + onTestLatency: () => { + }, + onChooseOutbound: () => { + } + }); + return container.replaceChildren(renderedWidget); + } + const renderedWidgets = sectionsWidget.data.map( + (section) => renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section, + onTestLatency: (tag) => { + replaceTestLatencyButtonsWithSkeleton(); + if (section.withTagSelect) { + return handleTestGroupLatency(tag); + } + return handleTestProxyLatency(tag); + }, + onChooseOutbound: (selector, tag) => { + handleChooseOutbound(selector, tag); + } + }) + ); + return container.replaceChildren(...renderedWidgets); +} +async function renderBandwidthWidget() { + console.log("renderBandwidthWidget"); + const traffic = store.get().bandwidthWidget; + const container = document.getElementById("dashboard-widget-traffic"); + if (traffic.loading || traffic.failed) { + const renderedWidget2 = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: _("Traffic"), + items: [ + { key: _("Uplink"), value: `${prettyBytes(traffic.data.up)}/s` }, + { key: _("Downlink"), value: `${prettyBytes(traffic.data.down)}/s` } + ] + }); + container.replaceChildren(renderedWidget); +} +async function renderTrafficTotalWidget() { + console.log("renderTrafficTotalWidget"); + const trafficTotalWidget = store.get().trafficTotalWidget; + const container = document.getElementById("dashboard-widget-traffic-total"); + if (trafficTotalWidget.loading || trafficTotalWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: _("Traffic Total"), + items: [ + { + key: _("Uplink"), + value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)) + }, + { + key: _("Downlink"), + value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)) + } + ] + }); + container.replaceChildren(renderedWidget); +} +async function renderSystemInfoWidget() { + console.log("renderSystemInfoWidget"); + const systemInfoWidget = store.get().systemInfoWidget; + const container = document.getElementById("dashboard-widget-system-info"); + if (systemInfoWidget.loading || systemInfoWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: _("System info"), + items: [ + { + key: _("Active Connections"), + value: String(systemInfoWidget.data.connections) + }, + { + key: _("Memory Usage"), + value: String(prettyBytes(systemInfoWidget.data.memory)) + } + ] + }); + container.replaceChildren(renderedWidget); +} +async function renderServicesInfoWidget() { + console.log("renderServicesInfoWidget"); + const servicesInfoWidget = store.get().servicesInfoWidget; + const container = document.getElementById("dashboard-widget-service-info"); + if (servicesInfoWidget.loading || servicesInfoWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: _("Services info"), + items: [ + { + key: _("Podkop"), + value: servicesInfoWidget.data.podkop ? _("\u2714 Enabled") : _("\u2718 Disabled"), + attributes: { + class: servicesInfoWidget.data.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" + } + }, + { + key: _("Sing-box"), + value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), + attributes: { + class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" + } + } + ] + }); + container.replaceChildren(renderedWidget); +} +async function onStoreUpdate(next, prev, diff) { + if (diff.sectionsWidget) { + renderSectionsWidget(); + } + if (diff.bandwidthWidget) { + renderBandwidthWidget(); + } + if (diff.trafficTotalWidget) { + renderTrafficTotalWidget(); + } + if (diff.systemInfoWidget) { + renderSystemInfoWidget(); + } + if (diff.servicesInfoWidget) { + renderServicesInfoWidget(); + } +} +async function initDashboardController() { + onMount("dashboard-status").then(() => { + store.unsubscribe(onStoreUpdate); + store.reset(); + store.subscribe(onStoreUpdate); + fetchDashboardSections(); + fetchServicesInfo(); + connectToClashSockets(); + }); +} return baseclass.extend({ ALLOWED_WITH_RUSSIA_INSIDE, BOOTSTRAP_DNS_SERVER_OPTIONS, @@ -573,14 +1866,34 @@ return baseclass.extend({ IP_CHECK_DOMAIN, REGIONAL_OPTIONS, STATUS_COLORS, + TabService, + TabServiceInstance, UPDATE_INTERVAL_OPTIONS, bulkValidate, - copyToClipboard, + coreService, + createBaseApiRequest, executeShellCommand, getBaseUrl, + getClashApiUrl, + getClashConfig, + getClashGroupDelay, + getClashProxies, + getClashVersion, + getClashWsUrl, + getConfigSections, + getDashboardSections, + getPodkopStatus, + getProxyUrlName, + getSingboxStatus, + initDashboardController, injectGlobalStyles, maskIP, + onMount, parseValueList, + renderDashboard, + triggerLatencyGroupTest, + triggerLatencyProxyTest, + triggerProxySelector, validateDNS, validateDomain, validateIPV4, 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 0b1e6c6..c84ff91 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -5,66 +5,78 @@ 'require view.podkop.configSection as configSection'; 'require view.podkop.diagnosticTab as diagnosticTab'; 'require view.podkop.additionalTab as additionalTab'; +'require view.podkop.dashboardTab as dashboardTab'; 'require view.podkop.utils as utils'; 'require view.podkop.main as main'; const EntryNode = { - async render() { - main.injectGlobalStyles(); + async render() { + main.injectGlobalStyles(); - const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); + const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); - // Main Section - const mainSection = podkopFormMap.section(form.TypedSection, 'main'); - mainSection.anonymous = true; - configSection.createConfigSection(mainSection); + // Main Section + const mainSection = podkopFormMap.section(form.TypedSection, 'main'); + mainSection.anonymous = true; - // Additional Settings Tab (main section) - additionalTab.createAdditionalSection(mainSection); + configSection.createConfigSection(mainSection); - // Diagnostics Tab (main section) - diagnosticTab.createDiagnosticsSection(mainSection); - const podkopFormMapPromise = podkopFormMap.render().then(node => { - // Set up diagnostics event handlers - diagnosticTab.setupDiagnosticsEventHandlers(node); + // Additional Settings Tab (main section) + additionalTab.createAdditionalSection(mainSection); - // Start critical error polling for all tabs + // 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 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')); - extraSection.anonymous = false; - extraSection.addremove = true; - extraSection.addbtntitle = _('Add Section'); - extraSection.multiple = true; - configSection.createConfigSection(extraSection); + // Add visibility change handler to manage error polling + document.addEventListener('visibilitychange', function () { + if (document.hidden) { + utils.stopErrorPolling(); + } else { + utils.startErrorPolling(); + } + }); - return podkopFormMapPromise; - } -} + return node; + }); + + // Extra Section + const extraSection = podkopFormMap.section( + form.TypedSection, + 'extra', + _('Extra configurations'), + ); + extraSection.anonymous = false; + extraSection.addremove = true; + extraSection.addbtntitle = _('Add Section'); + extraSection.multiple = true; + configSection.createConfigSection(extraSection); + + // Initial dashboard render + dashboardTab.createDashboardSection(mainSection); + + // Inject core service + main.coreService(); + + return podkopFormMapPromise; + }, +}; return view.extend(EntryNode); diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index 0567231..ef1d859 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-02 19:37+0500\n" -"PO-Revision-Date: 2025-09-30 15:18+0500\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" "Language-Team: none\n" "Language: ru\n" @@ -17,171 +17,6 @@ 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 "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 "DNS Protocol Type" -msgstr "Тип DNS протокола" - -msgid "Select DNS protocol to use" -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 "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 "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 "Invalid DNS server format. Example: 8.8.8.8" -msgstr "Неверный формат DNS-сервера. Пример: 8.8.8.8" - -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 "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 "Path must end with cache.db" -msgstr "Путь должен заканчиваться на cache.db" - -msgid "Path must contain at least one directory (like /tmp/cache.db)" -msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" - -msgid "Source Network Interface" -msgstr "Сетевой интерфейс источника" - -msgid "Select the network interface from which the traffic will originate" -msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" - -msgid "Interface monitoring" -msgstr "Мониторинг интерфейсов" - -msgid "Interface monitoring for bad WAN" -msgstr "Мониторинг интерфейсов для плохого WAN" - -msgid "Interface for monitoring" -msgstr "Интерфейс для мониторинга" - -msgid "Select the WAN interfaces to be monitored" -msgstr "Выберите WAN интерфейсы для мониторинга" - -msgid "Interface Monitoring Delay" -msgstr "Задержка при мониторинге интерфейсов" - -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" - -msgid "Delay value cannot be empty" -msgstr "Значение задержки не может быть пустым" - -msgid "Dont touch my DHCP!" -msgstr "Не трогать мой DHCP!" - -msgid "Podkop will not change the DHCP config" -msgstr "Podkop не будет изменять конфигурацию DHCP" - -msgid "Proxy download of lists" -msgstr "Загрузка списков через прокси" - -msgid "Downloading all lists via main Proxy/VPN" -msgstr "Загрузка всех списков через основной прокси/VPN" - -msgid "IP for exclusion" -msgstr "IP для исключения" - -msgid "Specify local IP addresses that will never use the configured route" -msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут" - -msgid "Local IPs" -msgstr "Локальные IP адреса" - -msgid "Enter valid IPv4 addresses" -msgstr "Введите действительные IPv4-адреса" - -msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)" -msgstr "Неверный формат IP. Используйте формат: X.X.X.X (например: 192.168.1.1)" - -msgid "IP address parts must be between 0 and 255" -msgstr "Части IP-адреса должны быть между 0 и 255" - -msgid "Mixed enable" -msgstr "Включить смешанный режим" - -msgid "Browser port: 2080" -msgstr "Порт браузера: 2080" - -msgid "URL must use one of the following protocols: " -msgstr "URL должен использовать один из следующих протоколов: " - -msgid "Invalid URL format" -msgstr "Неверный формат URL" - msgid "Basic Settings" msgstr "Основные настройки" @@ -216,71 +51,18 @@ msgid "Config without description" msgstr "Конфигурация без описания" msgid "" -"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " -"configs" +"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs" msgstr "" -"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для " -"сохранения других конфигураций" +"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для резервных конфигураций" -msgid "No active configuration found. At least one non-commented line is required." +msgid "No active configuration found. One configuration is required." msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка." -msgid "URL must start with vless:// or ss://" -msgstr "URL должен начинаться с vless:// или ss://" +msgid "Multiply active configurations found. Please leave one configuration." +msgstr "Найдено несколько активных конфигураций. Оставьте только одну." -msgid "Invalid Shadowsocks URL format: missing method and password separator \":\"" -msgstr "Неверный формат URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" - -msgid "Invalid Shadowsocks URL format" -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: missing or invalid server/port format" -msgstr "Неверный URL Shadowsocks: отсутствует или неверный формат сервера/порта" - -msgid "Invalid VLESS URL: missing UUID" -msgstr "Неверный URL VLESS: отсутствует UUID" - -msgid "Invalid VLESS URL: missing server address" -msgstr "Неверный URL VLESS: отсутствует адрес сервера" - -msgid "Invalid VLESS URL: missing server" -msgstr "Неверный URL VLESS: отсутствует сервер" - -msgid "Invalid VLESS URL: missing port" -msgstr "Неверный URL VLESS: отсутствует порт" - -msgid "Invalid VLESS URL: missing or invalid server/port format" -msgstr "Неверный URL VLESS: отсутствует или неверный формат сервера/порта" - -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 URL format: " -msgstr "Неверный формат URL: " +msgid "Invalid URL format:" +msgstr "Неверный формат URL:" msgid "Outbound Configuration" msgstr "Конфигурация исходящего соединения" @@ -288,12 +70,6 @@ msgstr "Конфигурация исходящего соединения" msgid "Enter complete outbound configuration in JSON format" msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" -msgid "JSON must contain at least type, server and server_port fields" -msgstr "JSON должен содержать как минимум поля type, server и server_port" - -msgid "Invalid JSON format" -msgstr "Неверный формат JSON" - msgid "URLTest Proxy Links" msgstr "Ссылки прокси для URLTest" @@ -315,8 +91,26 @@ 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 для резолвера доменов" +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 "Списки сообщества" @@ -328,21 +122,16 @@ msgid "Select predefined service for routing" msgstr "Выберите предустановленные сервисы для маршрутизации" msgid "Regional options cannot be used together" -msgstr "Нельзя использовать несколько региональных опций" +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 "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 "Тип пользовательского списка доменов" @@ -363,25 +152,19 @@ msgid "User Domains" msgstr "Пользовательские домены" msgid "Enter domain names without protocols (example: sub.example.com or example.com)" -msgstr "Введите доменные имена без указания протоколов (например: sub.example.com или example.com)" - -msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)" -msgstr "Введите имена доменов без протоколов (пример: sub.example.com или 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 "" -"Введите имена доменов, разделяя их запятой, пробелом или с новой строки. Вы можете добавлять комментарии после //" - -#, javascript-format -msgid "Invalid domain format: %s. Enter domain without protocol" -msgstr "Неверный формат домена: %s. Введите домен без протокола" +msgstr "Введите домены через запятую, пробел или с новой строки. Можно добавлять комментарии после //" msgid "At least one valid domain must be specified. Comments-only content is not allowed." -msgstr "" -"Должен быть указан хотя бы один действительный домен. Содержимое, состоящее только из комментариев, не допускается." +msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается." + +msgid "Validation errors:" +msgstr "Ошибки валидации:" msgid "Local Domain Lists" msgstr "Локальные списки доменов" @@ -395,17 +178,14 @@ msgstr "Пути к локальным спискам доменов" msgid "Enter the list file path" msgstr "Введите путь к файлу списка" -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы" - msgid "Remote Domain Lists" -msgstr "Удаленные списки доменов" +msgstr "Удалённые списки доменов" msgid "Download and use domain lists from remote URLs" -msgstr "Загрузка и использование списков доменов с удаленных URL" +msgstr "Загружать и использовать списки доменов с удалённых URL" msgid "Remote Domain URLs" -msgstr "URL удаленных доменов" +msgstr "URL удалённых доменов" msgid "Enter full URLs starting with http:// or https://" msgstr "Введите полные URL, начинающиеся с http:// или https://" @@ -423,58 +203,31 @@ msgid "Select how to add your custom subnets" msgstr "Выберите способ добавления пользовательских подсетей" msgid "Text List (comma/space/newline separated)" -msgstr "Текстовый список (разделенный запятыми/пробелами/новыми строками)" +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 "Invalid format. Use format: 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" +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-адреса, разделенные запятой, пробелом или новой строкой. Вы можете " -"добавлять комментарии после //" - -#, javascript-format -msgid "Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y" -msgstr "Неверный формат: %s. Используйте формат: X.X.X.X или X.X.X.X/Y" - -#, javascript-format -msgid "IP parts must be between 0 and 255 in: %s" -msgstr "Части IP-адреса должны быть между 0 и 255 в: %s" - -#, javascript-format -msgid "CIDR must be between 0 and 32 in: %s" -msgstr "CIDR должен быть между 0 и 32 в: %s" +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. Содержимое, состоящее только из комментариев, не " -"допускается." +msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." msgid "Remote Subnet Lists" -msgstr "Удаленные списки подсетей" +msgstr "Удалённые списки подсетей" msgid "Download and use subnet lists from remote URLs" -msgstr "Загрузка и использование списков подсетей с удаленных URL" +msgstr "Загружать и использовать списки подсетей с удалённых URL" msgid "Remote Subnet URLs" -msgstr "URL удаленных подсетей" +msgstr "URL удалённых подсетей" msgid "IP for full redirection" msgstr "IP для полного перенаправления" @@ -482,21 +235,219 @@ 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 "✘ Stopped" +msgstr "✘ Остановлен" + msgid "Copied!" msgstr "Скопировано!" msgid "Failed to copy: " msgstr "Не удалось скопировать: " +msgid "Loading..." +msgstr "Загрузка..." + msgid "Copy to Clipboard" -msgstr "Копировать в буфер обмена" +msgstr "Копировать в буфер" msgid "Close" msgstr "Закрыть" -msgid "Loading..." -msgstr "Загрузка..." - msgid "No output" msgstr "Нет вывода" @@ -507,7 +458,7 @@ msgid "FakeIP is not working in browser" msgstr "FakeIP не работает в браузере" msgid "Check DNS server on current device (PC, phone)" -msgstr "Проверьте DNS сервер на текущем устройстве (ПК, телефон)" +msgstr "Проверьте DNS-сервер на текущем устройстве (ПК, телефон)" msgid "Its must be router!" msgstr "Это должен быть роутер!" @@ -522,7 +473,7 @@ msgid "Proxy IP: " msgstr "Прокси IP: " msgid "Proxy is not working - same IP for both domains" -msgstr "Прокси не работает - одинаковый IP для обоих доменов" +msgstr "Прокси не работает — одинаковый IP для обоих доменов" msgid "IP: " msgstr "IP: " diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index e52267d..778c412 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -8,266 +8,32 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-02 19:37+0500\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" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:8 -msgid "Additional Settings" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:10 -msgid "Yacd enable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:15 -msgid "Exclude NTP" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:15 -msgid "Allows you to exclude NTP protocol traffic from the tunnel" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:20 -msgid "QUIC disable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:20 -msgid "For issues with the video stream" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:25 -msgid "List Update Frequency" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:25 -msgid "Select how often the lists will be updated" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:33 -#: htdocs/luci-static/resources/view/podkop/configSection.js:249 -msgid "DNS Protocol Type" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:33 -msgid "Select DNS protocol to use" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:34 -#: htdocs/luci-static/resources/view/podkop/configSection.js:250 -msgid "DNS over HTTPS (DoH)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:35 -#: htdocs/luci-static/resources/view/podkop/configSection.js:251 -msgid "DNS over TLS (DoT)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:36 -#: htdocs/luci-static/resources/view/podkop/configSection.js:252 -msgid "UDP (Unprotected DNS)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:41 -#: htdocs/luci-static/resources/view/podkop/configSection.js:258 -msgid "DNS Server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:41 -#: htdocs/luci-static/resources/view/podkop/configSection.js:258 -msgid "Select or enter DNS server address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:50 -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:77 -#: htdocs/luci-static/resources/view/podkop/configSection.js:268 -msgid "DNS server address cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:57 -#: htdocs/luci-static/resources/view/podkop/configSection.js:275 -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/additionalTab.js:63 -msgid "Bootstrap DNS server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:63 -msgid "The DNS server used to look up the IP address of an upstream DNS server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:83 -msgid "Invalid DNS server format. Example: 8.8.8.8" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:89 -msgid "DNS Rewrite TTL" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:89 -msgid "Time in seconds for DNS record caching (default: 60)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:95 -msgid "TTL value cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:100 -msgid "TTL must be a positive number" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:106 -msgid "Config File Path" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:106 -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:113 -msgid "Cache File Path" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:113 -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:121 -msgid "Cache file path cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:125 -msgid "Path must be absolute (start with /)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:129 -msgid "Path must end with cache.db" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:134 -msgid "Path must contain at least one directory (like /tmp/cache.db)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:140 -msgid "Source Network Interface" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:140 -msgid "Select the network interface from which the traffic will originate" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:164 -msgid "Interface monitoring" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:164 -msgid "Interface monitoring for bad WAN" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:169 -msgid "Interface for monitoring" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:169 -msgid "Select the WAN interfaces to be monitored" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:177 -msgid "Interface Monitoring Delay" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:177 -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:184 -msgid "Delay value cannot be empty" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:189 -msgid "Dont touch my DHCP!" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:189 -msgid "Podkop will not change the DHCP config" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:194 -msgid "Proxy download of lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:194 -msgid "Downloading all lists via main Proxy/VPN" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:200 -msgid "IP for exclusion" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:200 -msgid "Specify local IP addresses that will never use the configured route" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:205 -#: htdocs/luci-static/resources/view/podkop/configSection.js:574 -msgid "Local IPs" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:205 -#: htdocs/luci-static/resources/view/podkop/configSection.js:574 -msgid "Enter valid IPv4 addresses" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:213 -#: htdocs/luci-static/resources/view/podkop/configSection.js:582 -msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:217 -#: htdocs/luci-static/resources/view/podkop/configSection.js:488 -#: htdocs/luci-static/resources/view/podkop/configSection.js:586 -msgid "IP address parts must be between 0 and 255" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:222 -msgid "Mixed enable" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/additionalTab.js:222 -msgid "Browser port: 2080" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:13 -msgid "URL must use one of the following protocols: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:17 -msgid "Invalid URL format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:24 +#: htdocs/luci-static/resources/view/podkop/configSection.js:12 msgid "Basic Settings" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:26 +#: htdocs/luci-static/resources/view/podkop/configSection.js:18 msgid "Connection Type" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:26 +#: 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:32 +#: htdocs/luci-static/resources/view/podkop/configSection.js:30 msgid "Configuration Type" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:32 +#: htdocs/luci-static/resources/view/podkop/configSection.js:31 msgid "Select how to configure the proxy" msgstr "" @@ -283,125 +49,47 @@ msgstr "" msgid "URLTest" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:40 +#: htdocs/luci-static/resources/view/podkop/configSection.js:44 msgid "Proxy Configuration URL" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:64 +#: htdocs/luci-static/resources/view/podkop/configSection.js:81 msgid "Current config: " msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:67 -#: htdocs/luci-static/resources/view/podkop/configSection.js:71 -#: htdocs/luci-static/resources/view/podkop/configSection.js:77 +#: 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:82 +#: 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:100 -msgid "No active configuration found. At least one non-commented line is required." +#: 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:104 -msgid "URL must start with vless:// or ss://" +#: 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:116 -#: htdocs/luci-static/resources/view/podkop/configSection.js:121 -msgid "Invalid Shadowsocks URL format: missing method and password separator \":\"" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:125 -msgid "Invalid Shadowsocks URL format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:130 -msgid "Invalid Shadowsocks URL: missing server address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:132 -msgid "Invalid Shadowsocks URL: missing server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:134 -msgid "Invalid Shadowsocks URL: missing port" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:137 #: htdocs/luci-static/resources/view/podkop/configSection.js:157 -msgid "Invalid port number. Must be between 1 and 65535" +msgid "Invalid URL format:" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:140 -msgid "Invalid Shadowsocks URL: missing or invalid server/port format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:146 -msgid "Invalid VLESS URL: missing UUID" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:150 -msgid "Invalid VLESS URL: missing server address" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:152 -msgid "Invalid VLESS URL: missing server" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:154 -msgid "Invalid VLESS URL: missing port" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:160 -msgid "Invalid VLESS URL: missing or invalid server/port format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:164 -msgid "Invalid VLESS URL: missing query parameters" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:170 -msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:176 -msgid "Invalid VLESS URL: security must be one of tls, reality, none" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:180 -msgid "Invalid VLESS URL: missing pbk parameter for reality security" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:181 -msgid "Invalid VLESS URL: missing fp parameter for reality security" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:188 -msgid "Invalid URL format: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:192 +#: htdocs/luci-static/resources/view/podkop/configSection.js:165 msgid "Outbound Configuration" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:192 +#: 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:201 -msgid "JSON must contain at least type, server and server_port fields" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:205 -msgid "Invalid JSON format" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:209 +#: htdocs/luci-static/resources/view/podkop/configSection.js:190 msgid "URLTest Proxy Links" msgstr "" @@ -409,448 +97,858 @@ msgstr "" msgid "Shadowsocks UDP over TCP" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:214 +#: htdocs/luci-static/resources/view/podkop/configSection.js:215 msgid "Apply for SS2022" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:220 +#: htdocs/luci-static/resources/view/podkop/configSection.js:226 msgid "Network Interface" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:220 +#: 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:243 +#: htdocs/luci-static/resources/view/podkop/configSection.js:274 msgid "Domain Resolver" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:243 +#: 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:249 +#: 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:281 +#: 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:286 +#: htdocs/luci-static/resources/view/podkop/configSection.js:335 msgid "Service List" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:286 +#: htdocs/luci-static/resources/view/podkop/configSection.js:336 msgid "Select predefined service for routing" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:314 +#: 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:315 +#: 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:325 +#: htdocs/luci-static/resources/view/podkop/configSection.js:391 msgid "Russia inside restrictions" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:326 +#: 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:348 +#: htdocs/luci-static/resources/view/podkop/configSection.js:427 msgid "User Domain List Type" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:348 +#: 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:349 -#: htdocs/luci-static/resources/view/podkop/configSection.js:465 +#: 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:350 -#: htdocs/luci-static/resources/view/podkop/configSection.js:466 +#: 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:351 +#: htdocs/luci-static/resources/view/podkop/configSection.js:432 msgid "Text List" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:356 +#: htdocs/luci-static/resources/view/podkop/configSection.js:441 msgid "User Domains" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:356 +#: 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:365 -msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:370 +#: htdocs/luci-static/resources/view/podkop/configSection.js:469 msgid "User Domains List" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:370 +#: 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:398 -#, javascript-format -msgid "Invalid domain format: %s. Enter domain without protocol" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:405 +#: 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:411 +#: 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:411 -#: htdocs/luci-static/resources/view/podkop/configSection.js:445 +#: 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:416 +#: htdocs/luci-static/resources/view/podkop/configSection.js:522 msgid "Local Domain List Paths" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:416 -#: htdocs/luci-static/resources/view/podkop/configSection.js:450 +#: 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:425 -#: htdocs/luci-static/resources/view/podkop/configSection.js:459 -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:430 +#: htdocs/luci-static/resources/view/podkop/configSection.js:548 msgid "Remote Domain Lists" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:430 +#: 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:435 +#: htdocs/luci-static/resources/view/podkop/configSection.js:559 msgid "Remote Domain URLs" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:435 -#: htdocs/luci-static/resources/view/podkop/configSection.js:559 +#: 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:445 +#: htdocs/luci-static/resources/view/podkop/configSection.js:585 msgid "Local Subnet Lists" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:450 +#: htdocs/luci-static/resources/view/podkop/configSection.js:596 msgid "Local Subnet List Paths" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:464 +#: htdocs/luci-static/resources/view/podkop/configSection.js:622 msgid "User Subnet List Type" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:464 +#: 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:467 +#: 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:472 +#: htdocs/luci-static/resources/view/podkop/configSection.js:636 msgid "User Subnets" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:472 +#: 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:480 -msgid "Invalid format. Use format: X.X.X.X or X.X.X.X/Y" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:483 -msgid "IP address 0.0.0.0 is not allowed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:492 -msgid "CIDR must be between 0 and 32" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:497 +#: htdocs/luci-static/resources/view/podkop/configSection.js:664 msgid "User Subnets List" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:497 +#: 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:525 -#, javascript-format -msgid "Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:533 -#, javascript-format -msgid "IP parts must be between 0 and 255 in: %s" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:540 -#, javascript-format -msgid "CIDR must be between 0 and 32 in: %s" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/configSection.js:548 +#: 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:554 +#: htdocs/luci-static/resources/view/podkop/configSection.js:706 msgid "Remote Subnet Lists" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:554 +#: 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:559 +#: htdocs/luci-static/resources/view/podkop/configSection.js:717 msgid "Remote Subnet URLs" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:569 +#: htdocs/luci-static/resources/view/podkop/configSection.js:743 msgid "IP for full redirection" msgstr "" -#: htdocs/luci-static/resources/view/podkop/configSection.js:569 +#: 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/diagnosticTab.js:121 -msgid "Copied!" +#: 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/diagnosticTab.js:124 -msgid "Failed to copy: " +#: 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/diagnosticTab.js:272 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:303 -msgid "Copy to Clipboard" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:276 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:307 -msgid "Close" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:293 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:439 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:579 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:580 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:581 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:582 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:583 -msgid "Loading..." -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:326 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:388 -msgid "No output" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:338 -msgid "FakeIP is working in browser!" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:340 -msgid "FakeIP is not working in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:341 -msgid "Check DNS server on current device (PC, phone)" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:342 -msgid "Its must be router!" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:355 -msgid "Proxy working correctly" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:356 -msgid "Direct IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:357 -msgid "Proxy IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:359 -msgid "Proxy is not working - same IP for both domains" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:360 -msgid "IP: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:362 -msgid "Proxy check failed" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:368 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:373 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:378 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:382 -msgid "Check failed: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:368 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:373 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:378 -msgid "timeout" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:393 -msgid "Error: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:461 -msgid "Podkop Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:486 -msgid "Global check" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:488 -msgid "Click here for all the info" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:496 -msgid "Update Lists" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:498 -msgid "Lists Update Results" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:506 -msgid "Sing-box Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:527 -msgid "Check NFT Rules" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:529 -msgid "NFT Rules" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:532 -msgid "Check DNSMasq" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:534 -msgid "DNSMasq Configuration" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:542 -msgid "FakeIP Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:555 -msgid "DNS Status" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:564 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:780 -msgid "Main config" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:575 -msgid "Version Information" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:579 -msgid "Podkop: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:580 -msgid "LuCI App: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:581 -msgid "Sing-box: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:582 -msgid "OpenWrt Version: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:583 -msgid "Device Model: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:694 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:700 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:706 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:719 -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:720 -msgid "Unknown" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:729 -msgid "works in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:729 -msgid "does not work in browser" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:738 -msgid "works on router" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:738 -msgid "does not work on router" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:793 -msgid "Config: " -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:807 -msgid "Diagnostics" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:818 -msgid "Podkop" -msgstr "" - -#: htdocs/luci-static/resources/view/podkop/podkop.js:84 +#: htdocs/luci-static/resources/view/podkop/podkop.js:64 msgid "Extra configurations" msgstr "" -#: htdocs/luci-static/resources/view/podkop/podkop.js:87 +#: 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 +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 +msgid "✔ Running" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/main.js:1816 +msgid "✘ Stopped" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:137 +msgid "Copied!" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:143 +msgid "Failed to copy: " +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..." +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:351 +msgid "Copy to Clipboard" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:359 +msgid "Close" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:380 +#: htdocs/luci-static/resources/view/podkop/diagnosticTab.js:483 +msgid "No output" +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 +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" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:123 +msgid "DNS Rewrite TTL" +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 +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 +msgid "Cache file path cannot be empty" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:180 +msgid "Path must be absolute (start with /)" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:184 +msgid "Path must end with cache.db" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:189 +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:199 +msgid "Source Network Interface" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:200 +msgid "Select the network interface from which the traffic will originate" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:238 +msgid "Interface monitoring" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:239 +msgid "Interface monitoring for bad WAN" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:249 +msgid "Interface for monitoring" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:250 +msgid "Select the WAN interfaces to be monitored" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:274 +msgid "Interface Monitoring Delay" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:275 +msgid "Delay in milliseconds before reloading podkop after interface UP" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:283 +msgid "Delay value cannot be empty" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:292 +msgid "Dont touch my DHCP!" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:293 +msgid "Podkop will not change the DHCP config" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:303 +msgid "Proxy download of lists" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:304 +msgid "Downloading all lists via main Proxy/VPN" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:315 +msgid "IP for exclusion" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:316 +msgid "Specify local IP addresses that will never use the configured route" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:352 +msgid "Mixed enable" +msgstr "" + +#: htdocs/luci-static/resources/view/podkop/additionalTab.js:353 +msgid "Browser port: 2080" +msgstr "" diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 7682190..462e303 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -1123,15 +1123,16 @@ sing_box_configure_experimental() { config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db" config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true) - local yacd_enabled + local yacd_enabled external_controller_ui config_get_bool yacd_enabled "main" "yacd" 0 + log "Configuring Clash API" if [ "$yacd_enabled" -eq 1 ]; then - log "Configuring Clash API (yacd)" - local external_controller="0.0.0.0:9090" + log "YACD is enabled, enabling Clash API with downloadable YACD" "debug" local external_controller_ui="ui" - config=$(sing_box_cm_configure_clash_api "$config" "$external_controller" "$external_controller_ui") + config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER" "$external_controller_ui") else - log "Clash API (yacd) is disabled, skipping configuration." + log "YACD is disabled, enabling Clash API in online mode" "debug" + config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER") fi } diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 3710e6d..4745434 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -48,6 +48,8 @@ SB_DIRECT_OUTBOUND_TAG="direct-out" SB_MAIN_OUTBOUND_TAG="main-out" # Route SB_REJECT_RULE_TAG="reject-rule-tag" +# Experimental +SB_CLASH_API_CONTROLLER="0.0.0.0:9090" ## Lists GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main" diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 33f8703..ce66424 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -1335,8 +1335,8 @@ sing_box_cm_configure_cache_file() { # Configure the experimental clash_api section of a sing-box JSON configuration. # Arguments: # config: JSON configuration (string) -# external_controller: string, URL or path for the external controller -# external_ui: string, URL or path for the external UI +# external_controller: API listening address; Clash API will be disabled if empty +# external_ui: Optional path to static web resources to serve at http://{{external-controller}}/ui # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1352,8 +1352,8 @@ sing_box_cm_configure_clash_api() { --arg external_ui "$external_ui" \ '.experimental.clash_api = { external_controller: $external_controller, - external_ui: $external_ui - }' + } + + (if $external_ui != "" then { external_ui: $external_ui } else {} end)' } #######################################