mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-15 07:56:56 +03:00
Merge branch 'feat/yacd-exp' into feat/fe-app-podkop
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.idea
|
||||
fe-app-podkop/node_modules
|
||||
fe-app-podkop/.env
|
||||
|
||||
@@ -7,7 +7,7 @@ export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
ignores: ['node_modules'],
|
||||
ignores: ['node_modules', 'watch-upload.js'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
fe-app-podkop/src/clash/index.ts
Normal file
2
fe-app-podkop/src/clash/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './methods';
|
||||
28
fe-app-podkop/src/clash/methods/createBaseApiRequest.ts
Normal file
28
fe-app-podkop/src/clash/methods/createBaseApiRequest.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IBaseApiResponse } from '../types';
|
||||
|
||||
export async function createBaseApiRequest<T>(
|
||||
fetchFn: () => Promise<Response>,
|
||||
): Promise<IBaseApiResponse<T>> {
|
||||
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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
14
fe-app-podkop/src/clash/methods/getConfig.ts
Normal file
14
fe-app-podkop/src/clash/methods/getConfig.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||
import { getClashApiUrl } from '../../helpers';
|
||||
|
||||
export async function getClashConfig(): Promise<
|
||||
IBaseApiResponse<ClashAPI.Config>
|
||||
> {
|
||||
return createBaseApiRequest<ClashAPI.Config>(() =>
|
||||
fetch(`${getClashApiUrl()}/configs`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
20
fe-app-podkop/src/clash/methods/getGroupDelay.ts
Normal file
20
fe-app-podkop/src/clash/methods/getGroupDelay.ts
Normal file
@@ -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<IBaseApiResponse<ClashAPI.Delays>> {
|
||||
const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
|
||||
url,
|
||||
)}&timeout=${timeout}`;
|
||||
|
||||
return createBaseApiRequest<ClashAPI.Delays>(() =>
|
||||
fetch(endpoint, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
14
fe-app-podkop/src/clash/methods/getProxies.ts
Normal file
14
fe-app-podkop/src/clash/methods/getProxies.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||
import { getClashApiUrl } from '../../helpers';
|
||||
|
||||
export async function getClashProxies(): Promise<
|
||||
IBaseApiResponse<ClashAPI.Proxies>
|
||||
> {
|
||||
return createBaseApiRequest<ClashAPI.Proxies>(() =>
|
||||
fetch(`${getClashApiUrl()}/proxies`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
14
fe-app-podkop/src/clash/methods/getVersion.ts
Normal file
14
fe-app-podkop/src/clash/methods/getVersion.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||
import { getClashApiUrl } from '../../helpers';
|
||||
|
||||
export async function getClashVersion(): Promise<
|
||||
IBaseApiResponse<ClashAPI.Version>
|
||||
> {
|
||||
return createBaseApiRequest<ClashAPI.Version>(() =>
|
||||
fetch(`${getClashApiUrl()}/version`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
7
fe-app-podkop/src/clash/methods/index.ts
Normal file
7
fe-app-podkop/src/clash/methods/index.ts
Normal file
@@ -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';
|
||||
35
fe-app-podkop/src/clash/methods/triggerLatencyTest.ts
Normal file
35
fe-app-podkop/src/clash/methods/triggerLatencyTest.ts
Normal file
@@ -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<IBaseApiResponse<void>> {
|
||||
return createBaseApiRequest<void>(() =>
|
||||
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<IBaseApiResponse<void>> {
|
||||
return createBaseApiRequest<void>(() =>
|
||||
fetch(
|
||||
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
16
fe-app-podkop/src/clash/methods/triggerProxySelector.ts
Normal file
16
fe-app-podkop/src/clash/methods/triggerProxySelector.ts
Normal file
@@ -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<IBaseApiResponse<void>> {
|
||||
return createBaseApiRequest<void>(() =>
|
||||
fetch(`${getClashApiUrl()}/proxies/${selector}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: outbound }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
53
fe-app-podkop/src/clash/types.ts
Normal file
53
fe-app-podkop/src/clash/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type IBaseApiResponse<T> =
|
||||
| {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string, ProxyBase>;
|
||||
}
|
||||
|
||||
export type Delays = Record<string, number>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
@@ -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`;
|
||||
}
|
||||
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function getProxyUrlName(url: string) {
|
||||
try {
|
||||
const [_link, hash] = url.split('#');
|
||||
|
||||
if (!hash) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return decodeURIComponent(hash);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export async function onMount(id: string): Promise<HTMLElement> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
operationName: string,
|
||||
timeoutMessage = 'Operation timed out',
|
||||
timeoutMessage = _('Operation timed out'),
|
||||
): Promise<T> {
|
||||
let timeoutId;
|
||||
const start = performance.now();
|
||||
|
||||
25
fe-app-podkop/src/luci.d.ts
vendored
25
fe-app-podkop/src/luci.d.ts
vendored
@@ -1,3 +1,15 @@
|
||||
type HtmlTag = keyof HTMLElementTagNameMap;
|
||||
|
||||
type HtmlElement<T extends HtmlTag> = HTMLElementTagNameMap[T];
|
||||
|
||||
type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
|
||||
Omit<HtmlElement<T>, 'style' | 'children'> & {
|
||||
style?: string | Partial<CSSStyleDeclaration>;
|
||||
class?: string;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
declare global {
|
||||
const fs: {
|
||||
exec(
|
||||
@@ -10,6 +22,19 @@ declare global {
|
||||
code?: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
const E: <T extends HtmlTag>(
|
||||
type: T,
|
||||
attr?: HtmlAttributes<T> | null,
|
||||
children?: (Node | string)[] | Node | string,
|
||||
) => HTMLElementTagNameMap[T];
|
||||
|
||||
const uci: {
|
||||
load: (packages: string | string[]) => Promise<string>;
|
||||
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
|
||||
};
|
||||
|
||||
const _ = (_key: string) => string;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -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';
|
||||
|
||||
3
fe-app-podkop/src/podkop/index.ts
Normal file
3
fe-app-podkop/src/podkop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './methods';
|
||||
export * from './services';
|
||||
export * from './tabs';
|
||||
5
fe-app-podkop/src/podkop/methods/getConfigSections.ts
Normal file
5
fe-app-podkop/src/podkop/methods/getConfigSections.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Podkop } from '../types';
|
||||
|
||||
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
|
||||
return uci.load('podkop').then(() => uci.sections('podkop'));
|
||||
}
|
||||
153
fe-app-podkop/src/podkop/methods/getDashboardSections.ts
Normal file
153
fe-app-podkop/src/podkop/methods/getDashboardSections.ts
Normal file
@@ -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<IGetDashboardSectionsResponse> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
21
fe-app-podkop/src/podkop/methods/getPodkopStatus.ts
Normal file
21
fe-app-podkop/src/podkop/methods/getPodkopStatus.ts
Normal file
@@ -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' };
|
||||
}
|
||||
23
fe-app-podkop/src/podkop/methods/getSingboxStatus.ts
Normal file
23
fe-app-podkop/src/podkop/methods/getSingboxStatus.ts
Normal file
@@ -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' };
|
||||
}
|
||||
4
fe-app-podkop/src/podkop/methods/index.ts
Normal file
4
fe-app-podkop/src/podkop/methods/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './getConfigSections';
|
||||
export * from './getDashboardSections';
|
||||
export * from './getPodkopStatus';
|
||||
export * from './getSingboxStatus';
|
||||
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
@@ -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),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './tab.service';
|
||||
export * from './core.service';
|
||||
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
@@ -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<HTMLElement>('.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<HTMLElement>(
|
||||
'.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();
|
||||
2
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
2
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './renderDashboard';
|
||||
export * from './initDashboardController';
|
||||
@@ -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<StoreType>,
|
||||
) {
|
||||
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<void> {
|
||||
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();
|
||||
});
|
||||
}
|
||||
54
fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts
Normal file
54
fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts
Normal file
@@ -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: () => {},
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
125
fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts
Normal file
125
fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts
Normal file
@@ -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);
|
||||
}
|
||||
78
fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts
Normal file
78
fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts
Normal file
@@ -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);
|
||||
}
|
||||
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dashboard';
|
||||
56
fe-app-podkop/src/podkop/types.ts
Normal file
56
fe-app-podkop/src/podkop/types.ts
Normal file
@@ -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';
|
||||
};
|
||||
}
|
||||
121
fe-app-podkop/src/socket.ts
Normal file
121
fe-app-podkop/src/socket.ts
Normal file
@@ -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<string, WebSocket>();
|
||||
private listeners = new Map<string, Set<Listener>>();
|
||||
private connected = new Map<string, boolean>();
|
||||
private errorListeners = new Map<string, Set<ErrorListener>>();
|
||||
|
||||
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();
|
||||
179
fe-app-podkop/src/store.ts
Normal file
179
fe-app-podkop/src/store.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Podkop } from './podkop/types';
|
||||
|
||||
function jsonStableStringify<T, V>(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<string, V>,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function jsonEqual<A, B>(a: A, b: B): boolean {
|
||||
try {
|
||||
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||
|
||||
// eslint-disable-next-line
|
||||
class Store<T extends Record<string, any>> {
|
||||
private value: T;
|
||||
private readonly initial: T;
|
||||
private listeners = new Set<Listener<T>>();
|
||||
private lastHash = '';
|
||||
|
||||
constructor(initial: T) {
|
||||
this.value = initial;
|
||||
this.initial = structuredClone(initial);
|
||||
this.lastHash = jsonStableStringify(initial);
|
||||
}
|
||||
|
||||
get(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(next: Partial<T>): void {
|
||||
const prev = this.value;
|
||||
const merged = { ...prev, ...next };
|
||||
|
||||
if (jsonEqual(prev, merged)) return;
|
||||
|
||||
this.value = merged;
|
||||
this.lastHash = jsonStableStringify(merged);
|
||||
|
||||
const diff: Partial<T> = {};
|
||||
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<T> = {};
|
||||
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<T>): () => void {
|
||||
this.listeners.add(cb);
|
||||
cb(this.value, this.value, {});
|
||||
return () => this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
unsubscribe(cb: Listener<T>): void {
|
||||
this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
patch<K extends keyof T>(key: K, value: T[K]): void {
|
||||
this.set({ [key]: value } as unknown as Partial<T>);
|
||||
}
|
||||
|
||||
getKey<K extends keyof T>(key: K): T[K] {
|
||||
return this.value[key];
|
||||
}
|
||||
|
||||
subscribeKey<K extends keyof T>(
|
||||
key: K,
|
||||
cb: (value: T[K]) => void,
|
||||
): () => void {
|
||||
let prev = this.value[key];
|
||||
const wrapper: Listener<T> = (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<StoreType>(initialStore);
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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://'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') };
|
||||
}
|
||||
}
|
||||
|
||||
2
fe-app-podkop/tests/setup/global-mocks.ts
Normal file
2
fe-app-podkop/tests/setup/global-mocks.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// tests/setup/global-mocks.ts
|
||||
globalThis._ = (key: string) => key;
|
||||
@@ -4,5 +4,6 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
setupFiles: ['./tests/setup/global-mocks.ts'],
|
||||
},
|
||||
});
|
||||
|
||||
82
fe-app-podkop/watch-upload.js
Normal file
82
fe-app-podkop/watch-upload.js
Normal file
@@ -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);
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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') +
|
||||
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
|
||||
);
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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: "
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)'
|
||||
}
|
||||
|
||||
#######################################
|
||||
|
||||
Reference in New Issue
Block a user