Merge branch 'feat/yacd-exp' into feat/fe-app-podkop

This commit is contained in:
divocat
2025-10-07 17:16:36 +03:00
66 changed files with 4798 additions and 1373 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea .idea
fe-app-podkop/node_modules fe-app-podkop/node_modules
fe-app-podkop/.env

View File

@@ -7,7 +7,7 @@ export default [
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
{ {
ignores: ['node_modules'], ignores: ['node_modules', 'watch-upload.js'],
}, },
{ {
rules: { rules: {

View File

@@ -10,14 +10,19 @@
"build": "tsup src/main.ts", "build": "tsup src/main.ts",
"dev": "tsup src/main.ts --watch", "dev": "tsup src/main.ts --watch",
"test": "vitest", "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": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0", "@typescript-eslint/parser": "8.45.0",
"chokidar": "4.0.3",
"dotenv": "17.2.3",
"eslint": "9.36.0", "eslint": "9.36.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"glob": "11.0.3",
"prettier": "3.6.2", "prettier": "3.6.2",
"ssh2-sftp-client": "12.0.1",
"tsup": "8.5.0", "tsup": "8.5.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.45.0", "typescript-eslint": "8.45.0",

View File

@@ -0,0 +1,2 @@
export * from './types';
export * from './methods';

View 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'),
};
}
}

View 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' },
}),
);
}

View 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' },
}),
);
}

View 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' },
}),
);
}

View 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' },
}),
);
}

View 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';

View 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' },
},
),
);
}

View 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 }),
}),
);
}

View 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>;
}

View File

@@ -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);
}
}

View 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`;
}

View File

@@ -0,0 +1,13 @@
export function getProxyUrlName(url: string) {
try {
const [_link, hash] = url.split('#');
if (!hash) {
return '';
}
return decodeURIComponent(hash);
} catch {
return '';
}
}

View File

@@ -3,5 +3,7 @@ export * from './parseValueList';
export * from './injectGlobalStyles'; export * from './injectGlobalStyles';
export * from './withTimeout'; export * from './withTimeout';
export * from './executeShellCommand'; export * from './executeShellCommand';
export * from './copyToClipboard';
export * from './maskIP'; export * from './maskIP';
export * from './getProxyUrlName';
export * from './onMount';
export * from './getClashApiUrl';

View 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,
});
});
}

View 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;
}

View File

@@ -2,7 +2,7 @@ export async function withTimeout<T>(
promise: Promise<T>, promise: Promise<T>,
timeoutMs: number, timeoutMs: number,
operationName: string, operationName: string,
timeoutMessage = 'Operation timed out', timeoutMessage = _('Operation timed out'),
): Promise<T> { ): Promise<T> {
let timeoutId; let timeoutId;
const start = performance.now(); const start = performance.now();

View File

@@ -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 { declare global {
const fs: { const fs: {
exec( exec(
@@ -10,6 +22,19 @@ declare global {
code?: number; 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 {}; export {};

View File

@@ -1,7 +1,10 @@
'use strict'; 'use strict';
'require baseclass'; 'require baseclass';
'require fs'; 'require fs';
'require uci';
export * from './validators'; export * from './validators';
export * from './helpers'; export * from './helpers';
export * from './clash';
export * from './podkop';
export * from './constants'; export * from './constants';

View File

@@ -0,0 +1,3 @@
export * from './methods';
export * from './services';
export * from './tabs';

View File

@@ -0,0 +1,5 @@
import { Podkop } from '../types';
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
return uci.load('podkop').then(() => uci.sections('podkop'));
}

View 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,
};
}

View 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' };
}

View 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' };
}

View File

@@ -0,0 +1,4 @@
export * from './getConfigSections';
export * from './getDashboardSections';
export * from './getPodkopStatus';
export * from './getSingboxStatus';

View 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),
},
});
});
}

View File

@@ -0,0 +1,2 @@
export * from './tab.service';
export * from './core.service';

View 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();

View File

@@ -0,0 +1,2 @@
export * from './renderDashboard';
export * from './initDashboardController';

View File

@@ -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();
});
}

View 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: () => {},
}),
),
],
);
}

View 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);
}

View 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);
}

View File

@@ -0,0 +1 @@
export * from './dashboard';

View 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
View 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
View 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);

View File

@@ -23,4 +23,155 @@ export const GlobalStyles = `
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra { #cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
display: none; 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%;
}
}
`; `;

View File

@@ -4,20 +4,21 @@ import { ValidationResult } from './types';
export function validateDNS(value: string): ValidationResult { export function validateDNS(value: string): ValidationResult {
if (!value) { if (!value) {
return { valid: false, message: 'DNS server address cannot be empty' }; return { valid: false, message: _('DNS server address cannot be empty') };
} }
if (validateIPV4(value).valid) { if (validateIPV4(value).valid) {
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }
if (validateDomain(value).valid) { if (validateDomain(value).valid) {
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} }
return { return {
valid: false, valid: false,
message: message: _(
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH', 'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
),
}; };
} }

View File

@@ -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]*)?$/; /^(?=.{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)) { if (!domainRegex.test(domain)) {
return { valid: false, message: 'Invalid domain address' }; return { valid: false, message: _('Invalid domain address') };
} }
const hostname = domain.split('/')[0]; const hostname = domain.split('/')[0];
@@ -14,8 +14,8 @@ export function validateDomain(domain: string): ValidationResult {
const atLeastOneInvalidPart = parts.some((part) => part.length > 63); const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
if (atLeastOneInvalidPart) { 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') };
} }

View File

@@ -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])$/; /^(?:(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)) { 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') };
} }

View File

@@ -8,13 +8,14 @@ export function validateOutboundJson(value: string): ValidationResult {
if (!parsed.type || !parsed.server || !parsed.server_port) { if (!parsed.type || !parsed.server || !parsed.server_port) {
return { return {
valid: false, valid: false,
message: message: _(
'Outbound JSON must contain at least "type", "server" and "server_port" fields', 'Outbound JSON must contain at least "type", "server" and "server_port" fields',
),
}; };
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} catch { } catch {
return { valid: false, message: 'Invalid JSON format' }; return { valid: false, message: _('Invalid JSON format') };
} }
} }

View File

@@ -4,7 +4,7 @@ export function validatePath(value: string): ValidationResult {
if (!value) { if (!value) {
return { return {
valid: false, valid: false,
message: 'Path cannot be empty', message: _('Path cannot be empty'),
}; };
} }
@@ -19,7 +19,8 @@ export function validatePath(value: string): ValidationResult {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid path format. Path must start with "/" and contain valid characters', 'Invalid path format. Path must start with "/" and contain valid characters',
),
}; };
} }

View File

@@ -19,6 +19,6 @@ export function validateProxyUrl(url: string): ValidationResult {
return { return {
valid: false, valid: false,
message: 'URL must start with vless:// or ss:// or trojan://', message: _('URL must start with vless:// or ss:// or trojan://'),
}; };
} }

View File

@@ -5,7 +5,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!url.startsWith('ss://')) { if (!url.startsWith('ss://')) {
return { return {
valid: false, 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)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, 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) { if (!encryptedPart) {
return { return {
valid: false, 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(':')) { if (!decoded.includes(':')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid Shadowsocks URL: decoded credentials must contain method:password', 'Invalid Shadowsocks URL: decoded credentials must contain method:password',
),
}; };
} }
} catch (_e) { } catch (_e) {
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) { if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid Shadowsocks URL: missing method and password separator ":"', 'Invalid Shadowsocks URL: missing method and password separator ":"',
),
}; };
} }
} }
@@ -53,7 +55,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (!serverPart) { if (!serverPart) {
return { return {
valid: false, 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) { if (!server) {
return { return {
valid: false, valid: false,
message: 'Invalid Shadowsocks URL: missing server', message: _('Invalid Shadowsocks URL: missing server'),
}; };
} }
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) { 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); const portNum = parseInt(port, 10);
@@ -77,12 +82,15 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
if (isNaN(portNum) || portNum < 1 || portNum > 65535) { if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return { return {
valid: false, valid: false,
message: 'Invalid port number. Must be between 1 and 65535', message: _('Invalid port number. Must be between 1 and 65535'),
}; };
} }
} catch (_e) { } 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') };
} }

View File

@@ -8,14 +8,14 @@ export function validateSubnet(value: string): ValidationResult {
if (!subnetRegex.test(value)) { if (!subnetRegex.test(value)) {
return { return {
valid: false, 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('/'); const [ip, cidr] = value.split('/');
if (ip === '0.0.0.0') { 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); const ipCheck = validateIPV4(ip);
@@ -30,10 +30,10 @@ export function validateSubnet(value: string): ValidationResult {
if (cidrNum < 0 || cidrNum > 32) { if (cidrNum < 0 || cidrNum > 32) {
return { return {
valid: false, 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') };
} }

View File

@@ -5,14 +5,14 @@ export function validateTrojanUrl(url: string): ValidationResult {
if (!url.startsWith('trojan://')) { if (!url.startsWith('trojan://')) {
return { return {
valid: false, valid: false,
message: 'Invalid Trojan URL: must start with trojan://', message: _('Invalid Trojan URL: must start with trojan://'),
}; };
} }
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, 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) { if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
return { return {
valid: false, valid: false,
message: 'Invalid Trojan URL: must contain username, hostname and port', message: _(
'Invalid Trojan URL: must contain username, hostname and port',
),
}; };
} }
} catch (_e) { } 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') };
} }

View File

@@ -10,11 +10,11 @@ export function validateUrl(
if (!protocols.includes(parsedUrl.protocol)) { if (!protocols.includes(parsedUrl.protocol)) {
return { return {
valid: false, 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) { } catch (_e) {
return { valid: false, message: 'Invalid URL format' }; return { valid: false, message: _('Invalid URL format') };
} }
} }

View File

@@ -1,6 +1,5 @@
import { ValidationResult } from './types'; import { ValidationResult } from './types';
// TODO refactor current validation and add tests
export function validateVlessUrl(url: string): ValidationResult { export function validateVlessUrl(url: string): ValidationResult {
try { try {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
@@ -8,27 +7,27 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!url || /\s/.test(url)) { if (!url || /\s/.test(url)) {
return { return {
valid: false, valid: false,
message: 'Invalid VLESS URL: must not contain spaces', message: _('Invalid VLESS URL: must not contain spaces'),
}; };
} }
if (parsedUrl.protocol !== 'vless:') { if (parsedUrl.protocol !== 'vless:') {
return { return {
valid: false, valid: false,
message: 'Invalid VLESS URL: must start with vless://', message: _('Invalid VLESS URL: must start with vless://'),
}; };
} }
if (!parsedUrl.username) { if (!parsedUrl.username) {
return { valid: false, message: 'Invalid VLESS URL: missing UUID' }; return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
} }
if (!parsedUrl.hostname) { if (!parsedUrl.hostname) {
return { valid: false, message: 'Invalid VLESS URL: missing server' }; return { valid: false, message: _('Invalid VLESS URL: missing server') };
} }
if (!parsedUrl.port) { if (!parsedUrl.port) {
return { valid: false, message: 'Invalid VLESS URL: missing port' }; return { valid: false, message: _('Invalid VLESS URL: missing port') };
} }
if ( if (
@@ -38,15 +37,16 @@ export function validateVlessUrl(url: string): ValidationResult {
) { ) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535', 'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
),
}; };
} }
if (!parsedUrl.search) { if (!parsedUrl.search) {
return { return {
valid: false, 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)) { if (!type || !validTypes.includes(type)) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws', '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)) { if (!security || !validSecurities.includes(security)) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: security must be one of tls, reality, none', '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')) { if (!params.get('pbk')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: missing pbk parameter for reality security', 'Invalid VLESS URL: missing pbk parameter for reality security',
),
}; };
} }
if (!params.get('fp')) { if (!params.get('fp')) {
return { return {
valid: false, valid: false,
message: message: _(
'Invalid VLESS URL: missing fp parameter for reality security', 'Invalid VLESS URL: missing fp parameter for reality security',
),
}; };
} }
} }
return { valid: true, message: 'Valid' }; return { valid: true, message: _('Valid') };
} catch (_e) { } catch (_e) {
return { valid: false, message: 'Invalid VLESS URL: parsing failed' }; return { valid: false, message: _('Invalid VLESS URL: parsing failed') };
} }
} }

View File

@@ -0,0 +1,2 @@
// tests/setup/global-mocks.ts
globalThis._ = (key: string) => key;

View File

@@ -4,5 +4,6 @@ export default defineConfig({
test: { test: {
globals: true, globals: true,
environment: 'node', environment: 'node',
setupFiles: ['./tests/setup/global-mocks.ts'],
}, },
}); });

View 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);

View File

@@ -221,6 +221,18 @@
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== 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": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" 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" resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 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: assertion-error@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" 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" resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 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: brace-expansion@^1.1.7:
version "1.1.12" version "1.1.12"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
@@ -660,6 +686,16 @@ braces@^3.0.3:
dependencies: dependencies:
fill-range "^7.1.1" 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: bundle-require@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" 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" resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
chokidar@^4.0.3: chokidar@4.0.3, chokidar@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== 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" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 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: confbox@^0.1.8:
version "0.1.8" version "0.1.8"
resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" 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" resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== 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: cross-spawn@^7.0.6:
version "7.0.6" version "7.0.6"
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" 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" resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 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: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" 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" resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== 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" version "3.3.1"
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
@@ -1041,6 +1100,18 @@ glob-parent@^6.0.2:
dependencies: dependencies:
is-glob "^4.0.3" 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: glob@^10.3.10:
version "10.4.5" version "10.4.5"
resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" 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" resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 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: is-extglob@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1127,6 +1203,13 @@ jackspeak@^3.1.2:
optionalDependencies: optionalDependencies:
"@pkgjs/parseargs" "^0.11.0" "@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: joycon@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" 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" resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== 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: magic-string@^0.30.17:
version "0.30.19" version "0.30.19"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" 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" braces "^3.0.3"
picomatch "^2.3.1" 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: minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 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" object-assign "^4.0.1"
thenify-all "^1.0.0" 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: nanoid@^3.3.11:
version "3.3.11" version "3.3.11"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" 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" lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.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: pathe@^2.0.1, pathe@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" 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" resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 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: readdirp@^4.0.1:
version "4.1.2" version "4.1.2"
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
@@ -1483,6 +1600,16 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" 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: semver@^7.6.0:
version "7.7.2" version "7.7.2"
resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
@@ -1522,6 +1649,25 @@ source-map@0.8.0-beta.0:
dependencies: dependencies:
whatwg-url "^7.0.0" 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: stackback@0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" 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" emoji-regex "^9.2.2"
strip-ansi "^7.0.1" 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": "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 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" tinyglobby "^0.2.11"
tree-kill "^1.2.2" 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: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 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: dependencies:
prelude-ls "^1.2.1" 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: typescript-eslint@8.45.0:
version "8.45.0" version "8.45.0"
resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b" resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
@@ -1745,6 +1908,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" 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: vite-node@3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"

View File

@@ -88,7 +88,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(
@@ -113,7 +113,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(
@@ -342,7 +342,7 @@ function createAdditionalSection(mainSection) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = mainSection.taboption( o = mainSection.taboption(

View File

@@ -12,11 +12,11 @@ function createConfigSection(section) {
let o = s.tab('basic', _('Basic Settings')); let o = s.tab('basic', _('Basic Settings'));
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.ListValue, form.ListValue,
'mode', 'mode',
_('Connection Type'), _('Connection Type'),
_('Select between VPN and Proxy connection methods for traffic routing'), _('Select between VPN and Proxy connection methods for traffic routing'),
); );
o.value('proxy', 'Proxy'); o.value('proxy', 'Proxy');
o.value('vpn', 'VPN'); o.value('vpn', 'VPN');
@@ -24,11 +24,11 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.ListValue, form.ListValue,
'proxy_config_type', 'proxy_config_type',
_('Configuration Type'), _('Configuration Type'),
_('Select how to configure the proxy'), _('Select how to configure the proxy'),
); );
o.value('url', _('Connection URL')); o.value('url', _('Connection URL'));
o.value('outbound', _('Outbound Config')); o.value('outbound', _('Outbound Config'));
@@ -38,11 +38,11 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.TextValue, form.TextValue,
'proxy_string', 'proxy_string',
_('Proxy Configuration URL'), _('Proxy Configuration URL'),
'', '',
); );
o.depends('proxy_config_type', 'url'); o.depends('proxy_config_type', 'url');
o.rows = 5; o.rows = 5;
@@ -52,7 +52,7 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o.sectionDescriptions = new Map(); o.sectionDescriptions = new Map();
o.placeholder = 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) { o.renderWidget = function (section_id, option_index, cfgvalue) {
const original = form.TextValue.prototype.renderWidget.apply(this, [ const original = form.TextValue.prototype.renderWidget.apply(this, [
@@ -66,9 +66,9 @@ function createConfigSection(section) {
if (cfgvalue) { if (cfgvalue) {
try { try {
const activeConfig = cfgvalue const activeConfig = cfgvalue
.split('\n') .split('\n')
.map((line) => line.trim()) .map((line) => line.trim())
.find((line) => line && !line.startsWith('//')); .find((line) => line && !line.startsWith('//'));
if (activeConfig) { if (activeConfig) {
if (activeConfig.includes('#')) { if (activeConfig.includes('#')) {
@@ -76,24 +76,24 @@ function createConfigSection(section) {
if (label && label.trim()) { if (label && label.trim()) {
const decodedLabel = decodeURIComponent(label); const decodedLabel = decodeURIComponent(label);
const descDiv = E( const descDiv = E(
'div', 'div',
{ class: 'cbi-value-description' }, { class: 'cbi-value-description' },
_('Current config: ') + decodedLabel, _('Current config: ') + decodedLabel,
); );
container.appendChild(descDiv); container.appendChild(descDiv);
} else { } else {
const descDiv = E( const descDiv = E(
'div', 'div',
{ class: 'cbi-value-description' }, { class: 'cbi-value-description' },
_('Config without description'), _('Config without description'),
); );
container.appendChild(descDiv); container.appendChild(descDiv);
} }
} else { } else {
const descDiv = E( const descDiv = E(
'div', 'div',
{ class: 'cbi-value-description' }, { class: 'cbi-value-description' },
_('Config without description'), _('Config without description'),
); );
container.appendChild(descDiv); container.appendChild(descDiv);
} }
@@ -101,19 +101,19 @@ function createConfigSection(section) {
} catch (e) { } catch (e) {
console.error('Error parsing config label:', e); console.error('Error parsing config label:', e);
const descDiv = E( const descDiv = E(
'div', 'div',
{ class: 'cbi-value-description' }, { class: 'cbi-value-description' },
_('Config without description'), _('Config without description'),
); );
container.appendChild(descDiv); container.appendChild(descDiv);
} }
} else { } else {
const defaultDesc = E( const defaultDesc = E(
'div', 'div',
{ class: 'cbi-value-description' }, { class: 'cbi-value-description' },
_( _(
'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',
), ),
); );
container.appendChild(defaultDesc); container.appendChild(defaultDesc);
} }
@@ -129,20 +129,20 @@ function createConfigSection(section) {
try { try {
const activeConfigs = value const activeConfigs = value
.split('\n') .split('\n')
.map((line) => line.trim()) .map((line) => line.trim())
.filter((line) => !line.startsWith('//')) .filter((line) => !line.startsWith('//'))
.filter(Boolean); .filter(Boolean);
if (!activeConfigs.length) { if (!activeConfigs.length) {
return _( return _(
'No active configuration found. One configuration is required.', 'No active configuration found. One configuration is required.',
); );
} }
if (activeConfigs.length > 1) { if (activeConfigs.length > 1) {
return _( 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 true;
} }
return _(validation.message); return validation.message;
} catch (e) { } catch (e) {
return `${_('Invalid URL format:')} ${e?.message}`; return `${_('Invalid URL format:')} ${e?.message}`;
} }
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.TextValue, form.TextValue,
'outbound_json', 'outbound_json',
_('Outbound Configuration'), _('Outbound Configuration'),
_('Enter complete outbound configuration in JSON format'), _('Enter complete outbound configuration in JSON format'),
); );
o.depends('proxy_config_type', 'outbound'); o.depends('proxy_config_type', 'outbound');
o.rows = 10; o.rows = 10;
@@ -180,14 +180,14 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'urltest_proxy_links', 'urltest_proxy_links',
_('URLTest Proxy Links'), _('URLTest Proxy Links'),
); );
o.depends('proxy_config_type', 'urltest'); o.depends('proxy_config_type', 'urltest');
o.placeholder = 'vless://, ss://, trojan:// links'; o.placeholder = 'vless://, ss://, trojan:// links';
@@ -204,15 +204,15 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'ss_uot', 'ss_uot',
_('Shadowsocks UDP over TCP'), _('Shadowsocks UDP over TCP'),
_('Apply for SS2022'), _('Apply for SS2022'),
); );
o.default = '0'; o.default = '0';
o.depends('mode', 'proxy'); o.depends('mode', 'proxy');
@@ -220,11 +220,11 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
widgets.DeviceSelect, widgets.DeviceSelect,
'interface', 'interface',
_('Network Interface'), _('Network Interface'),
_('Select network interface for VPN connection'), _('Select network interface for VPN connection'),
); );
o.depends('mode', 'vpn'); o.depends('mode', 'vpn');
o.ucisection = s.section; o.ucisection = s.section;
@@ -262,17 +262,17 @@ function createConfigSection(section) {
// Reject wireless-related devices // Reject wireless-related devices
const isWireless = const isWireless =
type === 'wifi' || type === 'wireless' || type.includes('wlan'); type === 'wifi' || type === 'wireless' || type.includes('wlan');
return !isWireless; return !isWireless;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'domain_resolver_enabled', 'domain_resolver_enabled',
_('Domain Resolver'), _('Domain Resolver'),
_('Enable built-in DNS resolver for domains handled by this section'), _('Enable built-in DNS resolver for domains handled by this section'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
@@ -280,11 +280,11 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.ListValue, form.ListValue,
'domain_resolver_dns_type', 'domain_resolver_dns_type',
_('DNS Protocol Type'), _('DNS Protocol Type'),
_('Select the DNS protocol type for the domain resolver'), _('Select the DNS protocol type for the domain resolver'),
); );
o.value('doh', _('DNS over HTTPS (DoH)')); o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)')); o.value('dot', _('DNS over TLS (DoT)'));
@@ -295,11 +295,11 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Value, form.Value,
'domain_resolver_dns_server', 'domain_resolver_dns_server',
_('DNS Server'), _('DNS Server'),
_('Select or enter DNS server address'), _('Select or enter DNS server address'),
); );
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label)); o.value(key, _(label));
@@ -315,25 +315,25 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'community_lists_enabled', 'community_lists_enabled',
_('Community Lists'), _('Community Lists'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'community_lists', 'community_lists',
_('Service List'), _('Service List'),
_('Select predefined service for routing') + _('Select predefined service for routing') +
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>', ' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
); );
o.placeholder = 'Service list'; o.placeholder = 'Service list';
@@ -357,50 +357,50 @@ function createConfigSection(section) {
let notifications = []; let notifications = [];
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
newValues.includes(opt), newValues.includes(opt),
); );
if (selectedRegionalOptions.length > 1) { if (selectedRegionalOptions.length > 1) {
const lastSelected = const lastSelected =
selectedRegionalOptions[selectedRegionalOptions.length - 1]; selectedRegionalOptions[selectedRegionalOptions.length - 1];
const removedRegions = selectedRegionalOptions.slice(0, -1); const removedRegions = selectedRegionalOptions.slice(0, -1);
newValues = newValues.filter( newValues = newValues.filter(
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
); );
notifications.push( notifications.push(
E('p', { class: 'alert-message warning' }, [ E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Regional options cannot be used together')), E('strong', {}, _('Regional options cannot be used together')),
E('br'), E('br'),
_( _(
'Warning: %s cannot be used together with %s. Previous selections have been removed.', 'Warning: %s cannot be used together with %s. Previous selections have been removed.',
).format(removedRegions.join(', '), lastSelected), ).format(removedRegions.join(', '), lastSelected),
]), ]),
); );
} }
if (newValues.includes('russia_inside')) { if (newValues.includes('russia_inside')) {
const removedServices = newValues.filter( const removedServices = newValues.filter(
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
); );
if (removedServices.length > 0) { if (removedServices.length > 0) {
newValues = newValues.filter((v) => newValues = newValues.filter((v) =>
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
); );
notifications.push( notifications.push(
E('p', { class: 'alert-message warning' }, [ E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Russia inside restrictions')), E('strong', {}, _('Russia inside restrictions')),
E('br'), E('br'),
_( _(
'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.', 'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.',
).format( ).format(
main.ALLOWED_WITH_RUSSIA_INSIDE.map( main.ALLOWED_WITH_RUSSIA_INSIDE.map(
(key) => main.DOMAIN_LIST_OPTIONS[key], (key) => main.DOMAIN_LIST_OPTIONS[key],
) )
.filter((label) => label !== 'Russia inside') .filter((label) => label !== 'Russia inside')
.join(', '), .join(', '),
removedServices.join(', '), removedServices.join(', '),
), ),
]), ]),
); );
} }
} }
@@ -410,7 +410,7 @@ function createConfigSection(section) {
} }
notifications.forEach((notification) => notifications.forEach((notification) =>
ui.addNotification(null, notification), ui.addNotification(null, notification),
); );
lastValues = newValues; lastValues = newValues;
} catch (e) { } catch (e) {
@@ -421,11 +421,11 @@ function createConfigSection(section) {
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.ListValue, form.ListValue,
'user_domain_list_type', 'user_domain_list_type',
_('User Domain List Type'), _('User Domain List Type'),
_('Select how to add your custom domains'), _('Select how to add your custom domains'),
); );
o.value('disabled', _('Disabled')); o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List')); o.value('dynamic', _('Dynamic List'));
@@ -435,13 +435,13 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'user_domains', 'user_domains',
_('User Domains'), _('User Domains'),
_( _(
'Enter domain names without protocols (example: sub.example.com or example.com)', 'Enter domain names without protocols (example: sub.example.com or example.com)',
), ),
); );
o.placeholder = 'Domains list'; o.placeholder = 'Domains list';
o.depends('user_domain_list_type', 'dynamic'); o.depends('user_domain_list_type', 'dynamic');
@@ -459,20 +459,20 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.TextValue, form.TextValue,
'user_domains_text', 'user_domains_text',
_('User Domains List'), _('User Domains List'),
_( _(
'Enter domain names separated by comma, space or newline. You can add comments after //', 'Enter domain names separated by comma, space or newline. You can add comments after //',
), ),
); );
o.placeholder = 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.depends('user_domain_list_type', 'text');
o.rows = 8; o.rows = 8;
o.rmempty = false; o.rmempty = false;
@@ -487,7 +487,7 @@ function createConfigSection(section) {
if (!domains.length) { if (!domains.length) {
return _( 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) { if (!valid) {
const errors = results const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations .filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n'); return [_('Validation errors:'), ...errors].join('\n');
} }
@@ -505,22 +505,22 @@ function createConfigSection(section) {
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'local_domain_lists_enabled', 'local_domain_lists_enabled',
_('Local Domain Lists'), _('Local Domain Lists'),
_('Use the list from the router filesystem'), _('Use the list from the router filesystem'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'local_domain_lists', 'local_domain_lists',
_('Local Domain List Paths'), _('Local Domain List Paths'),
_('Enter the list file path'), _('Enter the list file path'),
); );
o.placeholder = '/path/file.lst'; o.placeholder = '/path/file.lst';
o.depends('local_domain_lists_enabled', '1'); o.depends('local_domain_lists_enabled', '1');
@@ -538,26 +538,26 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'remote_domain_lists_enabled', 'remote_domain_lists_enabled',
_('Remote Domain Lists'), _('Remote Domain Lists'),
_('Download and use domain lists from remote URLs'), _('Download and use domain lists from remote URLs'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'remote_domain_lists', 'remote_domain_lists',
_('Remote Domain URLs'), _('Remote Domain URLs'),
_('Enter full URLs starting with http:// or https://'), _('Enter full URLs starting with http:// or https://'),
); );
o.placeholder = 'URL'; o.placeholder = 'URL';
o.depends('remote_domain_lists_enabled', '1'); o.depends('remote_domain_lists_enabled', '1');
@@ -575,26 +575,26 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'local_subnet_lists_enabled', 'local_subnet_lists_enabled',
_('Local Subnet Lists'), _('Local Subnet Lists'),
_('Use the list from the router filesystem'), _('Use the list from the router filesystem'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'local_subnet_lists', 'local_subnet_lists',
_('Local Subnet List Paths'), _('Local Subnet List Paths'),
_('Enter the list file path'), _('Enter the list file path'),
); );
o.placeholder = '/path/file.lst'; o.placeholder = '/path/file.lst';
o.depends('local_subnet_lists_enabled', '1'); o.depends('local_subnet_lists_enabled', '1');
@@ -612,15 +612,15 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.ListValue, form.ListValue,
'user_subnet_list_type', 'user_subnet_list_type',
_('User Subnet List Type'), _('User Subnet List Type'),
_('Select how to add your custom subnets'), _('Select how to add your custom subnets'),
); );
o.value('disabled', _('Disabled')); o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List')); o.value('dynamic', _('Dynamic List'));
@@ -630,13 +630,13 @@ function createConfigSection(section) {
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'user_subnets', 'user_subnets',
_('User Subnets'), _('User Subnets'),
_( _(
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses', 'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
), ),
); );
o.placeholder = 'IP or subnet'; o.placeholder = 'IP or subnet';
o.depends('user_subnet_list_type', 'dynamic'); o.depends('user_subnet_list_type', 'dynamic');
@@ -654,20 +654,20 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.TextValue, form.TextValue,
'user_subnets_text', 'user_subnets_text',
_('User Subnets List'), _('User Subnets List'),
_( _(
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //', 'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
), ),
); );
o.placeholder = 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.depends('user_subnet_list_type', 'text');
o.rows = 10; o.rows = 10;
o.rmempty = false; o.rmempty = false;
@@ -682,7 +682,7 @@ function createConfigSection(section) {
if (!subnets.length) { if (!subnets.length) {
return _( 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) { if (!valid) {
const errors = results const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations .filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n'); return [_('Validation errors:'), ...errors].join('\n');
} }
@@ -700,22 +700,22 @@ function createConfigSection(section) {
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'remote_subnet_lists_enabled', 'remote_subnet_lists_enabled',
_('Remote Subnet Lists'), _('Remote Subnet Lists'),
_('Download and use subnet lists from remote URLs'), _('Download and use subnet lists from remote URLs'),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'remote_subnet_lists', 'remote_subnet_lists',
_('Remote Subnet URLs'), _('Remote Subnet URLs'),
_('Enter full URLs starting with http:// or https://'), _('Enter full URLs starting with http:// or https://'),
); );
o.placeholder = 'URL'; o.placeholder = 'URL';
o.depends('remote_subnet_lists_enabled', '1'); o.depends('remote_subnet_lists_enabled', '1');
@@ -733,28 +733,28 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.Flag, form.Flag,
'all_traffic_from_ip_enabled', 'all_traffic_from_ip_enabled',
_('IP for full redirection'), _('IP for full redirection'),
_( _(
'Specify local IP addresses whose traffic will always use the configured route', 'Specify local IP addresses whose traffic will always use the configured route',
), ),
); );
o.default = '0'; o.default = '0';
o.rmempty = false; o.rmempty = false;
o.ucisection = s.section; o.ucisection = s.section;
o = s.taboption( o = s.taboption(
'basic', 'basic',
form.DynamicList, form.DynamicList,
'all_traffic_ip', 'all_traffic_ip',
_('Local IPs'), _('Local IPs'),
_('Enter valid IPv4 addresses'), _('Enter valid IPv4 addresses'),
); );
o.placeholder = 'IP'; o.placeholder = 'IP';
o.depends('all_traffic_from_ip_enabled', '1'); o.depends('all_traffic_from_ip_enabled', '1');
@@ -772,7 +772,7 @@ function createConfigSection(section) {
return true; return true;
} }
return _(validation.message); return validation.message;
}; };
} }

View File

@@ -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);

View File

@@ -5,66 +5,78 @@
'require view.podkop.configSection as configSection'; 'require view.podkop.configSection as configSection';
'require view.podkop.diagnosticTab as diagnosticTab'; 'require view.podkop.diagnosticTab as diagnosticTab';
'require view.podkop.additionalTab as additionalTab'; 'require view.podkop.additionalTab as additionalTab';
'require view.podkop.dashboardTab as dashboardTab';
'require view.podkop.utils as utils'; 'require view.podkop.utils as utils';
'require view.podkop.main as main'; 'require view.podkop.main as main';
const EntryNode = { const EntryNode = {
async render() { async render() {
main.injectGlobalStyles(); main.injectGlobalStyles();
const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']);
// Main Section // Main Section
const mainSection = podkopFormMap.section(form.TypedSection, 'main'); const mainSection = podkopFormMap.section(form.TypedSection, 'main');
mainSection.anonymous = true; mainSection.anonymous = true;
configSection.createConfigSection(mainSection);
// Additional Settings Tab (main section) configSection.createConfigSection(mainSection);
additionalTab.createAdditionalSection(mainSection);
// Diagnostics Tab (main section) // Additional Settings Tab (main section)
diagnosticTab.createDiagnosticsSection(mainSection); additionalTab.createAdditionalSection(mainSection);
const podkopFormMapPromise = podkopFormMap.render().then(node => {
// Set up diagnostics event handlers
diagnosticTab.setupDiagnosticsEventHandlers(node);
// 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(); 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 // Add visibility change handler to manage error polling
const extraSection = podkopFormMap.section(form.TypedSection, 'extra', _('Extra configurations')); document.addEventListener('visibilitychange', function () {
extraSection.anonymous = false; if (document.hidden) {
extraSection.addremove = true; utils.stopErrorPolling();
extraSection.addbtntitle = _('Add Section'); } else {
extraSection.multiple = true; utils.startErrorPolling();
configSection.createConfigSection(extraSection); }
});
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); return view.extend(EntryNode);

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PODKOP\n" "Project-Id-Version: PODKOP\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-02 19:37+0500\n" "POT-Creation-Date: 2025-10-07 16:55+0300\n"
"PO-Revision-Date: 2025-09-30 15:18+0500\n" "PO-Revision-Date: 2025-10-07 23:45+0300\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -17,171 +17,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "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" msgid "Basic Settings"
msgstr "Основные настройки" msgstr "Основные настройки"
@@ -216,71 +51,18 @@ msgid "Config without description"
msgstr "Конфигурация без описания" msgstr "Конфигурация без описания"
msgid "" msgid ""
"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " "Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs"
"configs"
msgstr "" 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 "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка." msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка."
msgid "URL must start with vless:// or ss://" msgid "Multiply active configurations found. Please leave one configuration."
msgstr "URL должен начинаться с vless:// или ss://" msgstr "Найдено несколько активных конфигураций. Оставьте только одну."
msgid "Invalid Shadowsocks URL format: missing method and password separator \":\"" msgid "Invalid URL format:"
msgstr "Неверный формат URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" msgstr "Неверный формат URL:"
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 "Outbound Configuration" msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения" msgstr "Конфигурация исходящего соединения"
@@ -288,12 +70,6 @@ msgstr "Конфигурация исходящего соединения"
msgid "Enter complete outbound configuration in JSON format" msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" 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" msgid "URLTest Proxy Links"
msgstr "Ссылки прокси для URLTest" msgstr "Ссылки прокси для URLTest"
@@ -315,8 +91,26 @@ msgstr "Резолвер доменов"
msgid "Enable built-in DNS resolver for domains handled by this section" msgid "Enable built-in DNS resolver for domains handled by this section"
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе" msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "Select the DNS protocol type for the domain resolver" 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" msgid "Community Lists"
msgstr "Списки сообщества" msgstr "Списки сообщества"
@@ -328,21 +122,16 @@ msgid "Select predefined service for routing"
msgstr "Выберите предустановленные сервисы для маршрутизации" msgstr "Выберите предустановленные сервисы для маршрутизации"
msgid "Regional options cannot be used together" 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." msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
msgid "Russia inside restrictions" msgid "Russia inside restrictions"
msgstr "Ограничения Russia inside" 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."
msgid "" msgstr "Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора."
"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" msgid "User Domain List Type"
msgstr "Тип пользовательского списка доменов" msgstr "Тип пользовательского списка доменов"
@@ -363,25 +152,19 @@ msgid "User Domains"
msgstr "Пользовательские домены" msgstr "Пользовательские домены"
msgid "Enter domain names without protocols (example: sub.example.com or example.com)" msgid "Enter domain names without protocols (example: sub.example.com or example.com)"
msgstr "Введите доменные имена без указания протоколов (например: sub.example.com или 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)"
msgid "User Domains List" msgid "User Domains List"
msgstr "Список пользовательских доменов" msgstr "Список пользовательских доменов"
msgid "Enter domain names separated by comma, space or newline. You can add comments after //" msgid "Enter domain names separated by comma, space or newline. You can add comments after //"
msgstr "" msgstr "Введите домены через запятую, пробел или с новой строки. Можно добавлять комментарии после //"
"Введите имена доменов, разделяя их запятой, пробелом или с новой строки. Вы можете добавлять комментарии после //"
#, javascript-format
msgid "Invalid domain format: %s. Enter domain without protocol"
msgstr "Неверный формат домена: %s. Введите домен без протокола"
msgid "At least one valid domain must be specified. Comments-only content is not allowed." 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" msgid "Local Domain Lists"
msgstr "Локальные списки доменов" msgstr "Локальные списки доменов"
@@ -395,17 +178,14 @@ msgstr "Пути к локальным спискам доменов"
msgid "Enter the list file path" msgid "Enter the list file path"
msgstr "Введите путь к файлу списка" msgstr "Введите путь к файлу списка"
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
msgid "Remote Domain Lists" msgid "Remote Domain Lists"
msgstr "Удаленные списки доменов" msgstr "Удалённые списки доменов"
msgid "Download and use domain lists from remote URLs" msgid "Download and use domain lists from remote URLs"
msgstr "Загрузка и использование списков доменов с удаленных URL" msgstr "Загружать и использовать списки доменов с удалённых URL"
msgid "Remote Domain URLs" msgid "Remote Domain URLs"
msgstr "URL удаленных доменов" msgstr "URL удалённых доменов"
msgid "Enter full URLs starting with http:// or https://" msgid "Enter full URLs starting with http:// or https://"
msgstr "Введите полные URL, начинающиеся с http:// или https://" msgstr "Введите полные URL, начинающиеся с http:// или https://"
@@ -423,58 +203,31 @@ msgid "Select how to add your custom subnets"
msgstr "Выберите способ добавления пользовательских подсетей" msgstr "Выберите способ добавления пользовательских подсетей"
msgid "Text List (comma/space/newline separated)" msgid "Text List (comma/space/newline separated)"
msgstr "Текстовый список (разделенный запятыми/пробелами/новыми строками)" msgstr "Текстовый список (через запятую, пробел или новую строку)"
msgid "User Subnets" msgid "User Subnets"
msgstr "Пользовательские подсети" msgstr "Пользовательские подсети"
msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses" msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses"
msgstr "Введите подсети в нотации CIDR (пример: 103.21.244.0/22) или отдельные IP-адреса" 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"
msgid "User Subnets List" msgid "User Subnets List"
msgstr "Список пользовательских подсетей" msgstr "Список пользовательских подсетей"
msgid "" msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //"
"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments " msgstr "Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии после //"
"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 "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
msgstr "" msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
"Должна быть указана хотя бы одна действительная подсеть или IP. Содержимое, состоящее только из комментариев, не "
"допускается."
msgid "Remote Subnet Lists" msgid "Remote Subnet Lists"
msgstr "Удаленные списки подсетей" msgstr "Удалённые списки подсетей"
msgid "Download and use subnet lists from remote URLs" msgid "Download and use subnet lists from remote URLs"
msgstr "Загрузка и использование списков подсетей с удаленных URL" msgstr "Загружать и использовать списки подсетей с удалённых URL"
msgid "Remote Subnet URLs" msgid "Remote Subnet URLs"
msgstr "URL удаленных подсетей" msgstr "URL удалённых подсетей"
msgid "IP for full redirection" msgid "IP for full redirection"
msgstr "IP для полного перенаправления" msgstr "IP для полного перенаправления"
@@ -482,21 +235,219 @@ msgstr "IP для полного перенаправления"
msgid "Specify local IP addresses whose traffic will always use the configured route" msgid "Specify local IP addresses whose traffic will always use the configured route"
msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут" 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: недопустимый порт (165535)"
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!" msgid "Copied!"
msgstr "Скопировано!" msgstr "Скопировано!"
msgid "Failed to copy: " msgid "Failed to copy: "
msgstr "Не удалось скопировать: " msgstr "Не удалось скопировать: "
msgid "Loading..."
msgstr "Загрузка..."
msgid "Copy to Clipboard" msgid "Copy to Clipboard"
msgstr "Копировать в буфер обмена" msgstr "Копировать в буфер"
msgid "Close" msgid "Close"
msgstr "Закрыть" msgstr "Закрыть"
msgid "Loading..."
msgstr "Загрузка..."
msgid "No output" msgid "No output"
msgstr "Нет вывода" msgstr "Нет вывода"
@@ -507,7 +458,7 @@ msgid "FakeIP is not working in browser"
msgstr "FakeIP не работает в браузере" msgstr "FakeIP не работает в браузере"
msgid "Check DNS server on current device (PC, phone)" msgid "Check DNS server on current device (PC, phone)"
msgstr "Проверьте DNS сервер на текущем устройстве (ПК, телефон)" msgstr "Проверьте DNS-сервер на текущем устройстве (ПК, телефон)"
msgid "Its must be router!" msgid "Its must be router!"
msgstr "Это должен быть роутер!" msgstr "Это должен быть роутер!"
@@ -522,7 +473,7 @@ msgid "Proxy IP: "
msgstr "Прокси IP: " msgstr "Прокси IP: "
msgid "Proxy is not working - same IP for both domains" msgid "Proxy is not working - same IP for both domains"
msgstr "Прокси не работает - одинаковый IP для обоих доменов" msgstr "Прокси не работает одинаковый IP для обоих доменов"
msgid "IP: " msgid "IP: "
msgstr "IP: " msgstr "IP: "

File diff suppressed because it is too large Load Diff

View File

@@ -1123,15 +1123,16 @@ sing_box_configure_experimental() {
config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db" config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db"
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true) 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 config_get_bool yacd_enabled "main" "yacd" 0
log "Configuring Clash API"
if [ "$yacd_enabled" -eq 1 ]; then if [ "$yacd_enabled" -eq 1 ]; then
log "Configuring Clash API (yacd)" log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
local external_controller="0.0.0.0:9090"
local external_controller_ui="ui" 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 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 fi
} }

View File

@@ -48,6 +48,8 @@ SB_DIRECT_OUTBOUND_TAG="direct-out"
SB_MAIN_OUTBOUND_TAG="main-out" SB_MAIN_OUTBOUND_TAG="main-out"
# Route # Route
SB_REJECT_RULE_TAG="reject-rule-tag" SB_REJECT_RULE_TAG="reject-rule-tag"
# Experimental
SB_CLASH_API_CONTROLLER="0.0.0.0:9090"
## Lists ## Lists
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main" GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"

View File

@@ -1335,8 +1335,8 @@ sing_box_cm_configure_cache_file() {
# Configure the experimental clash_api section of a sing-box JSON configuration. # Configure the experimental clash_api section of a sing-box JSON configuration.
# Arguments: # Arguments:
# config: JSON configuration (string) # config: JSON configuration (string)
# external_controller: string, URL or path for the external controller # external_controller: API listening address; Clash API will be disabled if empty
# external_ui: string, URL or path for the external UI # external_ui: Optional path to static web resources to serve at http://{{external-controller}}/ui
# Outputs: # Outputs:
# Writes updated JSON configuration to stdout # Writes updated JSON configuration to stdout
# Example: # Example:
@@ -1352,8 +1352,8 @@ sing_box_cm_configure_clash_api() {
--arg external_ui "$external_ui" \ --arg external_ui "$external_ui" \
'.experimental.clash_api = { '.experimental.clash_api = {
external_controller: $external_controller, external_controller: $external_controller,
external_ui: $external_ui }
}' + (if $external_ui != "" then { external_ui: $external_ui } else {} end)'
} }
####################################### #######################################