Update diagnostics and domain UI

This commit is contained in:
ushan0v
2026-06-18 08:45:51 +03:00
parent 59a13cc772
commit e7e9188962
11 changed files with 1171 additions and 921 deletions
+3 -6
View File
@@ -60,12 +60,9 @@ sh <(wget -O - https://raw.githubusercontent.com/ushan0v/podkop-plus/main/instal
Podkop Plus расширяет набор условий, которые можно использовать в правилах: Podkop Plus расширяет набор условий, которые можно использовать в правилах:
- Домены (`domain_suffix`) - Домены (`domain_suffix`): обычные значения работают как суффиксы доменов. Для других типов используйте теги `full:example.com`, `keyword:example` и `regex:^example\.`.
- IP-адреса (`ip_cidr`) - IP-адреса (`ip_cidr`)
- Точный полный домен (`domain`) - Исходные IP-адреса (`source_ip_cidr`)
- Ключевое слово домена (`domain_keyword`)
- Регулярное выражение домена (`domain_regex`)
- Исходные IP-адреса (`source_cidr`)
- Порт (`port` / `port_range`) - Порт (`port` / `port_range`)
- Полностью маршрутизированные IP-адреса - Полностью маршрутизированные IP-адреса
- Встроенные наборы правил - Встроенные наборы правил
@@ -160,4 +157,4 @@ ByeDPI доступен как действие отдельной секции.
- 💳 **Карты РФ / СБП / Tinkoff Pay:** [Донат на CloudTips](https://pay.cloudtips.ru/p/385e5af2) - 💳 **Карты РФ / СБП / Tinkoff Pay:** [Донат на CloudTips](https://pay.cloudtips.ru/p/385e5af2)
- 💎 **TON / USDT (сеть TON):** `UQBuYHpmHorHquBItuuUsNJJs5lWoP6Bi0cgY-gM5RCQvYgC` - 💎 **TON / USDT (сеть TON):** `UQBuYHpmHorHquBItuuUsNJJs5lWoP6Bi0cgY-gM5RCQvYgC`
- 🟢 **USDT (сеть TRC-20):** `TR4SXiL9sV2GwNz8mDbJkN8SmK9jQ3GuZF` - 🟢 **USDT (сеть TRC-20):** `TR4SXiL9sV2GwNz8mDbJkN8SmK9jQ3GuZF`
- 🪙 **USDT (сеть ERC-20 / BEP-20):** `0x21F86eF81d33A449037d6E750D2504BF225EAA9d` - 🪙 **USDT (сеть ERC-20 / BEP-20):** `0x21F86eF81d33A449037d6E750D2504BF225EAA9d`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+25 -31
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PODKOP PLUS\n" "Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-10 22:19+0300\n" "POT-Creation-Date: 2026-06-18 06:38+0300\n"
"PO-Revision-Date: 2026-06-10 22:19+0300\n" "PO-Revision-Date: 2026-06-18 06:38+0300\n"
"Last-Translator: ushan0v\n" "Last-Translator: ushan0v\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -77,6 +77,9 @@ msgstr "Все, кроме выбранных"
msgid "All servers" msgid "All servers"
msgstr "Все серверы" msgstr "Все серверы"
msgid "Allowed domain prefixes are full:, keyword:, and regex:"
msgstr "Разрешённые префиксы доменов: full:, keyword: и regex:"
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
@@ -182,9 +185,6 @@ msgstr "Не удалось сохранить сервер"
msgid "Cascade connection" msgid "Cascade connection"
msgstr "Каскадное подключение" msgstr "Каскадное подключение"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
msgid "Cascade connection cannot use the current section" msgid "Cascade connection cannot use the current section"
msgstr "Каскадное подключение не может использовать текущую секцию" msgstr "Каскадное подключение не может использовать текущую секцию"
@@ -368,12 +368,6 @@ msgstr "Не резолвится"
msgid "Domain and IP Lists" msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs" msgstr "Списки доменов и IPs"
msgid "Domain keyword"
msgstr "Ключевое слово домена"
msgid "Domain regex"
msgstr "Регулярное выражение домена"
msgid "Domain Resolver" msgid "Domain Resolver"
msgstr "Резолвер доменов" msgstr "Резолвер доменов"
@@ -446,9 +440,6 @@ msgstr "Введите полный объект outbound sing-box"
msgid "Enter the subscription URL" msgid "Enter the subscription URL"
msgstr "Введите URL подписки" msgstr "Введите URL подписки"
msgid "Exact full domain"
msgstr "Точный полный домен"
msgid "Exclude country from URLTest" msgid "Exclude country from URLTest"
msgstr "Исключить страну из URLTest" msgstr "Исключить страну из URLTest"
@@ -551,6 +542,9 @@ msgstr "Таймаут рукопожатия"
msgid "Hide filtered servers" msgid "Hide filtered servers"
msgstr "Скрывать отфильтрованные серверы" msgstr "Скрывать отфильтрованные серверы"
msgid "Hide values"
msgstr "Скрывать значения"
msgid "Host" msgid "Host"
msgstr "Хост" msgstr "Хост"
@@ -794,8 +788,8 @@ msgstr "JSON outbound"
msgid "Key path" msgid "Key path"
msgstr "Путь к ключу" msgstr "Путь к ключу"
msgid "Keyword must not contain spaces" msgid "Keyword must not contain spaces or commas"
msgstr "Ключевое слово не должно содержать пробелы" msgstr "Ключевое слово не должно содержать пробелы или запятые"
msgid "Latency test failed" msgid "Latency test failed"
msgstr "" msgstr ""
@@ -845,18 +839,6 @@ msgstr "Совпадение по IP-адресам или подсетям на
msgid "Match destination ports. Use a single port or a range" msgid "Match destination ports. Use a single port or a range"
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон" msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
msgid "Match domains containing a substring"
msgstr "Совпадение по доменам, содержащим подстроку"
msgid "Match domains including all subdomains"
msgstr "Совпадение по доменам, включая все их поддомены"
msgid "Match domains using a regular expression"
msgstr "Совпадение по доменам с использованием регулярного выражения"
msgid "Match only one exact full domain name"
msgstr "Совпадение только по одному точному полному доменному имени"
msgid "Match source IPs or subnets" msgid "Match source IPs or subnets"
msgstr "Совпадение по исходным IP-адресам или подсетям" msgstr "Совпадение по исходным IP-адресам или подсетям"
@@ -1034,6 +1016,9 @@ msgstr "Приостановить обновление"
msgid "Pending" msgid "Pending"
msgstr "Ожидает запуска" msgstr "Ожидает запуска"
msgid "Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions."
msgstr "Обычные значения работают как суффиксы доменов. Используйте full:, keyword: или regex: для точных доменов, ключевых слов и регулярных выражений."
msgid "Podkop Plus Error" msgid "Podkop Plus Error"
msgstr "Ошибка Podkop Plus" msgstr "Ошибка Podkop Plus"
@@ -1148,6 +1133,9 @@ msgstr "Сброс"
msgid "Regional options cannot be used together" msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно" msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Regular expression must not contain spaces or commas"
msgstr "Регулярное выражение не должно содержать пробелы или запятые"
msgid "Remove" msgid "Remove"
msgstr "Удалить" msgstr "Удалить"
@@ -1256,6 +1244,9 @@ msgstr "Выберите предустановленный набор прав
msgid "Select a specific section for downloading external resources" msgid "Select a specific section for downloading external resources"
msgstr "Выберите конкретную секцию для скачки внешних ресурсов" msgstr "Выберите конкретную секцию для скачки внешних ресурсов"
msgid "Select a transit section"
msgstr "Выберите секцию-транзит"
msgid "Select an enabled proxy, JSON outbound, or VPN section" msgid "Select an enabled proxy, JSON outbound, or VPN section"
msgstr "Выберите включенную секцию proxy, JSON outbound или VPN" msgstr "Выберите включенную секцию proxy, JSON outbound или VPN"
@@ -1670,6 +1661,9 @@ msgstr "Укажите IPv4-адрес"
msgid "Use an IPv4 listen address" msgid "Use an IPv4 listen address"
msgstr "Укажите IPv4-адрес прослушивания" msgstr "Укажите IPv4-адрес прослушивания"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
msgid "Use CIDR prefixes like 192.168.1.0/24" msgid "Use CIDR prefixes like 192.168.1.0/24"
msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24" msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24"
@@ -1685,9 +1679,6 @@ msgstr "Используйте буквы, цифры, точки, подчёр
msgid "Use sing-box duration format like 1d, 12h or 30m" msgid "Use sing-box duration format like 1d, 12h or 30m"
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m" msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
msgid "Select a transit section"
msgstr "Выберите секцию-транзит"
msgid "Used by" msgid "Used by"
msgstr "Используется" msgstr "Используется"
@@ -1703,6 +1694,9 @@ msgstr "Валидно"
msgid "Validation errors:" msgid "Validation errors:"
msgstr "Ошибки валидации:" msgstr "Ошибки валидации:"
msgid "Value cannot be empty"
msgstr "Значение не может быть пустым"
msgid "Value must not contain control characters" msgid "Value must not contain control characters"
msgstr "Значение не должно содержать управляющие символы" msgstr "Значение не должно содержать управляющие символы"
@@ -2,13 +2,20 @@ import { renderButton } from '../button/renderButton';
import { copyToClipboard } from '../../helpers/copyToClipboard'; import { copyToClipboard } from '../../helpers/copyToClipboard';
import { downloadAsTxt } from '../../helpers/downloadAsTxt'; import { downloadAsTxt } from '../../helpers/downloadAsTxt';
interface ModalTextContext {
maskValues: boolean;
}
interface RenderModalOptions { interface RenderModalOptions {
getText?: () => string | Promise<string>; getText?: (context: ModalTextContext) => string | Promise<string>;
refreshMs?: number; refreshMs?: number;
initialAutoRefresh?: boolean; initialAutoRefresh?: boolean;
showAutoRefreshToggle?: boolean; showAutoRefreshToggle?: boolean;
showMaskValuesToggle?: boolean;
initialMaskValues?: boolean;
startAtEnd?: boolean; startAtEnd?: boolean;
autoRefreshLabel?: string; autoRefreshLabel?: string;
maskValuesLabel?: string;
} }
export function renderModal( export function renderModal(
@@ -19,13 +26,16 @@ export function renderModal(
let currentText = text ?? ''; let currentText = text ?? '';
let refreshInFlight = false; let refreshInFlight = false;
let pendingRefresh = false; let pendingRefresh = false;
let pendingForcedRefresh = false;
let refreshSessionId = 0; let refreshSessionId = 0;
let timer: ReturnType<typeof setInterval> | undefined; let timer: ReturnType<typeof setInterval> | undefined;
let observer: MutationObserver | undefined; let observer: MutationObserver | undefined;
let autoRefreshEnabled = let autoRefreshEnabled =
options?.initialAutoRefresh ?? Boolean(options?.getText); options?.initialAutoRefresh ?? Boolean(options?.getText);
let maskValuesEnabled = options?.initialMaskValues ?? true;
let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd); let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd);
let autoRefreshInput: HTMLInputElement | undefined; let autoRefreshInput: HTMLInputElement | undefined;
let maskValuesInput: HTMLInputElement | undefined;
const codeEl = E('code', {}, currentText) as HTMLElement; const codeEl = E('code', {}, currentText) as HTMLElement;
const contentEl = E( const contentEl = E(
@@ -44,6 +54,7 @@ export function renderModal(
const destroyLiveRefresh = () => { const destroyLiveRefresh = () => {
refreshSessionId += 1; refreshSessionId += 1;
pendingRefresh = false; pendingRefresh = false;
pendingForcedRefresh = false;
stopRefreshTimer(); stopRefreshTimer();
observer?.disconnect(); observer?.disconnect();
@@ -94,8 +105,12 @@ export function renderModal(
} }
}; };
const refreshText = async () => { const refreshText = async (force = false) => {
if (!options?.getText || !autoRefreshEnabled || refreshInFlight) { if (
!options?.getText ||
(!force && !autoRefreshEnabled) ||
refreshInFlight
) {
return; return;
} }
@@ -107,9 +122,11 @@ export function renderModal(
const sessionId = refreshSessionId; const sessionId = refreshSessionId;
try { try {
const nextText = await options.getText(); const nextText = await options.getText({
maskValues: maskValuesEnabled,
});
if (!body.isConnected || !autoRefreshEnabled) { if (!body.isConnected || (!force && !autoRefreshEnabled)) {
return; return;
} }
@@ -124,10 +141,12 @@ export function renderModal(
refreshInFlight = false; refreshInFlight = false;
if (pendingRefresh) { if (pendingRefresh) {
const shouldForceRefresh = pendingForcedRefresh;
pendingRefresh = false; pendingRefresh = false;
pendingForcedRefresh = false;
if (autoRefreshEnabled && body.isConnected) { if ((shouldForceRefresh || autoRefreshEnabled) && body.isConnected) {
void refreshText(); void refreshText(shouldForceRefresh);
} }
} }
} }
@@ -146,6 +165,20 @@ export function renderModal(
void refreshText(); void refreshText();
}; };
const requestForcedRefresh = () => {
if (!options?.getText) {
return;
}
if (refreshInFlight) {
pendingRefresh = true;
pendingForcedRefresh = true;
return;
}
void refreshText(true);
};
const startRefreshTimer = () => { const startRefreshTimer = () => {
if ( if (
!options?.getText || !options?.getText ||
@@ -165,6 +198,7 @@ export function renderModal(
autoRefreshEnabled = nextValue; autoRefreshEnabled = nextValue;
refreshSessionId += 1; refreshSessionId += 1;
pendingRefresh = false; pendingRefresh = false;
pendingForcedRefresh = false;
if (autoRefreshInput) { if (autoRefreshInput) {
autoRefreshInput.checked = nextValue; autoRefreshInput.checked = nextValue;
@@ -179,6 +213,19 @@ export function renderModal(
stopRefreshTimer(); stopRefreshTimer();
}; };
const setMaskValuesEnabled = (nextValue: boolean) => {
maskValuesEnabled = nextValue;
refreshSessionId += 1;
pendingRefresh = false;
pendingForcedRefresh = false;
if (maskValuesInput) {
maskValuesInput.checked = nextValue;
}
requestForcedRefresh();
};
const footerChildren: HTMLElement[] = [ const footerChildren: HTMLElement[] = [
renderButton({ renderButton({
classNames: ['cbi-button-apply'], classNames: ['cbi-button-apply'],
@@ -221,6 +268,27 @@ export function renderModal(
); );
} }
if (options?.getText && options?.showMaskValuesToggle) {
maskValuesInput = document.createElement('input');
maskValuesInput.type = 'checkbox';
maskValuesInput.className = 'cbi-input-checkbox';
maskValuesInput.checked = maskValuesEnabled;
maskValuesInput.addEventListener('change', () => {
setMaskValuesEnabled(maskValuesInput!.checked);
});
footerChildren.unshift(
E('label', { class: 'pdk-partial-modal__checkbox' }, [
maskValuesInput,
E(
'span',
{ class: 'pdk-partial-modal__checkbox-text' },
options.maskValuesLabel ?? _('Hide values'),
),
]) as HTMLElement,
);
}
const body = E('div', { class: 'pdk-partial-modal__body' }, [ const body = E('div', { class: 'pdk-partial-modal__body' }, [
E('div', {}, [ E('div', {}, [
contentEl, contentEl,
@@ -334,10 +334,15 @@ export const PodkopShellMethods = {
[], [],
'/etc/init.d/podkop-plus', '/etc/init.d/podkop-plus',
), ),
globalCheck: async () => globalCheck: async (masked = true) =>
callBaseMethod<unknown>(Podkop.AvailableMethods.GLOBAL_CHECK), callBaseMethod<unknown>(
showSingBoxConfig: async () => Podkop.AvailableMethods.GLOBAL_CHECK,
callBaseMethod<unknown>(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), [masked ? 'masked' : 'raw'],
),
showSingBoxConfig: async (masked = true) =>
callBaseMethod<unknown>(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG, [
masked ? 'masked' : 'raw',
]),
checkLogs: async () => checkLogs: async () =>
callBaseMethod<unknown>(Podkop.AvailableMethods.CHECK_LOGS), callBaseMethod<unknown>(Podkop.AvailableMethods.CHECK_LOGS),
checkSingBoxLogs: async () => checkSingBoxLogs: async () =>
@@ -670,9 +670,24 @@ async function handleShowGlobalCheck() {
const globalCheck = await PodkopShellMethods.globalCheck(); const globalCheck = await PodkopShellMethods.globalCheck();
if (globalCheck.success) { if (globalCheck.success) {
const getGlobalCheckText = async ({ maskValues = true } = {}) => {
const latestGlobalCheck =
await PodkopShellMethods.globalCheck(maskValues);
if (!latestGlobalCheck.success) {
throw latestGlobalCheck;
}
return (latestGlobalCheck.data as string) ?? '';
};
ui.showModal( ui.showModal(
_('Global check'), _('Global check'),
renderModal(globalCheck.data as string, 'global_check'), renderModal(globalCheck.data as string, 'global_check', {
getText: getGlobalCheckText,
initialAutoRefresh: false,
showMaskValuesToggle: true,
}),
); );
} else { } else {
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', globalCheck); logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', globalCheck);
@@ -728,11 +743,27 @@ async function handleShowSingBoxConfig() {
const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig();
if (showSingBoxConfig.success) { if (showSingBoxConfig.success) {
const getSingBoxConfigText = async ({ maskValues = true } = {}) => {
const latestSingBoxConfig =
await PodkopShellMethods.showSingBoxConfig(maskValues);
if (!latestSingBoxConfig.success) {
throw latestSingBoxConfig;
}
return JSON.stringify(latestSingBoxConfig.data, null, 2);
};
ui.showModal( ui.showModal(
_('Show sing-box config'), _('Show sing-box config'),
renderModal( renderModal(
JSON.stringify(showSingBoxConfig.data, null, 2), JSON.stringify(showSingBoxConfig.data, null, 2),
'show_sing_box_config', 'show_sing_box_config',
{
getText: getSingBoxConfigText,
initialAutoRefresh: false,
showMaskValuesToggle: true,
},
), ),
); );
} else { } else {
@@ -2585,8 +2585,13 @@ var PodkopShellMethods = {
[], [],
"/etc/init.d/podkop-plus" "/etc/init.d/podkop-plus"
), ),
globalCheck: async () => callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), globalCheck: async (masked = true) => callBaseMethod(
showSingBoxConfig: async () => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), Podkop.AvailableMethods.GLOBAL_CHECK,
[masked ? "masked" : "raw"]
),
showSingBoxConfig: async (masked = true) => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG, [
masked ? "masked" : "raw"
]),
checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS),
checkSingBoxLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_SING_BOX_LOGS), checkSingBoxLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_SING_BOX_LOGS),
getSystemInfo: async () => callBaseMethod( getSystemInfo: async () => callBaseMethod(
@@ -6869,12 +6874,15 @@ function renderModal(text, name, options) {
let currentText = text ?? ""; let currentText = text ?? "";
let refreshInFlight = false; let refreshInFlight = false;
let pendingRefresh = false; let pendingRefresh = false;
let pendingForcedRefresh = false;
let refreshSessionId = 0; let refreshSessionId = 0;
let timer; let timer;
let observer; let observer;
let autoRefreshEnabled = options?.initialAutoRefresh ?? Boolean(options?.getText); let autoRefreshEnabled = options?.initialAutoRefresh ?? Boolean(options?.getText);
let maskValuesEnabled = options?.initialMaskValues ?? true;
let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd); let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd);
let autoRefreshInput; let autoRefreshInput;
let maskValuesInput;
const codeEl = E("code", {}, currentText); const codeEl = E("code", {}, currentText);
const contentEl = E( const contentEl = E(
"pre", "pre",
@@ -6890,6 +6898,7 @@ function renderModal(text, name, options) {
const destroyLiveRefresh = () => { const destroyLiveRefresh = () => {
refreshSessionId += 1; refreshSessionId += 1;
pendingRefresh = false; pendingRefresh = false;
pendingForcedRefresh = false;
stopRefreshTimer(); stopRefreshTimer();
observer?.disconnect(); observer?.disconnect();
observer = void 0; observer = void 0;
@@ -6925,8 +6934,8 @@ function renderModal(text, name, options) {
}); });
} }
}; };
const refreshText = async () => { const refreshText = async (force = false) => {
if (!options?.getText || !autoRefreshEnabled || refreshInFlight) { if (!options?.getText || !force && !autoRefreshEnabled || refreshInFlight) {
return; return;
} }
if (!body.isConnected) { if (!body.isConnected) {
@@ -6935,8 +6944,10 @@ function renderModal(text, name, options) {
refreshInFlight = true; refreshInFlight = true;
const sessionId = refreshSessionId; const sessionId = refreshSessionId;
try { try {
const nextText = await options.getText(); const nextText = await options.getText({
if (!body.isConnected || !autoRefreshEnabled) { maskValues: maskValuesEnabled
});
if (!body.isConnected || !force && !autoRefreshEnabled) {
return; return;
} }
if (sessionId !== refreshSessionId) { if (sessionId !== refreshSessionId) {
@@ -6948,9 +6959,11 @@ function renderModal(text, name, options) {
} finally { } finally {
refreshInFlight = false; refreshInFlight = false;
if (pendingRefresh) { if (pendingRefresh) {
const shouldForceRefresh = pendingForcedRefresh;
pendingRefresh = false; pendingRefresh = false;
if (autoRefreshEnabled && body.isConnected) { pendingForcedRefresh = false;
void refreshText(); if ((shouldForceRefresh || autoRefreshEnabled) && body.isConnected) {
void refreshText(shouldForceRefresh);
} }
} }
} }
@@ -6965,6 +6978,17 @@ function renderModal(text, name, options) {
} }
void refreshText(); void refreshText();
}; };
const requestForcedRefresh = () => {
if (!options?.getText) {
return;
}
if (refreshInFlight) {
pendingRefresh = true;
pendingForcedRefresh = true;
return;
}
void refreshText(true);
};
const startRefreshTimer = () => { const startRefreshTimer = () => {
if (!options?.getText || !autoRefreshEnabled || timer || typeof document === "undefined") { if (!options?.getText || !autoRefreshEnabled || timer || typeof document === "undefined") {
return; return;
@@ -6977,6 +7001,7 @@ function renderModal(text, name, options) {
autoRefreshEnabled = nextValue; autoRefreshEnabled = nextValue;
refreshSessionId += 1; refreshSessionId += 1;
pendingRefresh = false; pendingRefresh = false;
pendingForcedRefresh = false;
if (autoRefreshInput) { if (autoRefreshInput) {
autoRefreshInput.checked = nextValue; autoRefreshInput.checked = nextValue;
} }
@@ -6987,6 +7012,16 @@ function renderModal(text, name, options) {
} }
stopRefreshTimer(); stopRefreshTimer();
}; };
const setMaskValuesEnabled = (nextValue) => {
maskValuesEnabled = nextValue;
refreshSessionId += 1;
pendingRefresh = false;
pendingForcedRefresh = false;
if (maskValuesInput) {
maskValuesInput.checked = nextValue;
}
requestForcedRefresh();
};
const footerChildren = [ const footerChildren = [
renderButton({ renderButton({
classNames: ["cbi-button-apply"], classNames: ["cbi-button-apply"],
@@ -7028,6 +7063,25 @@ ${currentText}
]) ])
); );
} }
if (options?.getText && options?.showMaskValuesToggle) {
maskValuesInput = document.createElement("input");
maskValuesInput.type = "checkbox";
maskValuesInput.className = "cbi-input-checkbox";
maskValuesInput.checked = maskValuesEnabled;
maskValuesInput.addEventListener("change", () => {
setMaskValuesEnabled(maskValuesInput.checked);
});
footerChildren.unshift(
E("label", { class: "pdk-partial-modal__checkbox" }, [
maskValuesInput,
E(
"span",
{ class: "pdk-partial-modal__checkbox-text" },
options.maskValuesLabel ?? _("Hide values")
)
])
);
}
const body = E("div", { class: "pdk-partial-modal__body" }, [ const body = E("div", { class: "pdk-partial-modal__body" }, [
E("div", {}, [ E("div", {}, [
contentEl, contentEl,
@@ -8149,9 +8203,20 @@ async function handleShowGlobalCheck() {
try { try {
const globalCheck = await PodkopShellMethods.globalCheck(); const globalCheck = await PodkopShellMethods.globalCheck();
if (globalCheck.success) { if (globalCheck.success) {
const getGlobalCheckText = async ({ maskValues = true } = {}) => {
const latestGlobalCheck = await PodkopShellMethods.globalCheck(maskValues);
if (!latestGlobalCheck.success) {
throw latestGlobalCheck;
}
return latestGlobalCheck.data ?? "";
};
ui.showModal( ui.showModal(
_("Global check"), _("Global check"),
renderModal(globalCheck.data, "global_check") renderModal(globalCheck.data, "global_check", {
getText: getGlobalCheckText,
initialAutoRefresh: false,
showMaskValuesToggle: true
})
); );
} else { } else {
logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck); logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck);
@@ -8198,11 +8263,23 @@ async function handleShowSingBoxConfig() {
try { try {
const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig();
if (showSingBoxConfig.success) { if (showSingBoxConfig.success) {
const getSingBoxConfigText = async ({ maskValues = true } = {}) => {
const latestSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(maskValues);
if (!latestSingBoxConfig.success) {
throw latestSingBoxConfig;
}
return JSON.stringify(latestSingBoxConfig.data, null, 2);
};
ui.showModal( ui.showModal(
_("Show sing-box config"), _("Show sing-box config"),
renderModal( renderModal(
JSON.stringify(showSingBoxConfig.data, null, 2), JSON.stringify(showSingBoxConfig.data, null, 2),
"show_sing_box_config" "show_sing_box_config",
{
getText: getSingBoxConfigText,
initialAutoRefresh: false,
showMaskValuesToggle: true
}
) )
); );
} else { } else {
@@ -781,8 +781,8 @@ function validateKeyword(_section_id, value) {
return true; return true;
} }
if (/\s/.test(value)) { if (/[,\s]/.test(value)) {
return _("Keyword must not contain spaces"); return _("Keyword must not contain spaces or commas");
} }
return true; return true;
@@ -1514,9 +1514,60 @@ function analyzeTextListValue(value, validateItem, emptyMessage, options = {}) {
} }
function analyzeDomainSuffixText(value) { function analyzeDomainSuffixText(value) {
const validateDomainCondition = (domain) => {
const normalized = `${domain || ""}`.trim();
if (normalized.includes("/")) {
return { valid: false, message: _("Invalid domain address") };
}
return main.validateDomain(normalized, true);
};
return analyzeTextListValue( return analyzeTextListValue(
value, value,
(item) => main.validateDomain(item, true), (item) => {
const colonIndex = item.indexOf(":");
const prefix = colonIndex > 0 ? item.slice(0, colonIndex) : "";
const body = colonIndex > 0 ? item.slice(colonIndex + 1) : item;
if (!prefix) {
return validateDomainCondition(body);
}
if (!["full", "keyword", "regex"].includes(prefix)) {
return {
valid: false,
message: _("Allowed domain prefixes are full:, keyword:, and regex:"),
};
}
if (!body.length) {
return { valid: false, message: _("Value cannot be empty") };
}
if (prefix === "full") {
return validateDomainCondition(body);
}
if (prefix === "keyword") {
const validation = validateKeyword(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
}
if (/[,\s]/.test(body)) {
return {
valid: false,
message: _("Regular expression must not contain spaces or commas"),
};
}
const validation = validateRegex(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
},
_("At least one valid domain must be specified."), _("At least one valid domain must be specified."),
{ {
normalizeDuplicateValue: (item) => `${item}`.toLowerCase(), normalizeDuplicateValue: (item) => `${item}`.toLowerCase(),
@@ -1524,6 +1575,54 @@ function analyzeDomainSuffixText(value) {
); );
} }
function domainValuesWithPrefix(section_id, key, prefix) {
return getConfigListValues(section_id, key).map((value) =>
prefix ? `${prefix}:${value}` : value,
);
}
function domainTextValuesWithPrefix(section_id, key, prefix) {
const legacyText = uci.get(UCI_PACKAGE, section_id, `${key}_text`);
if (!legacyText) {
return [];
}
return main.parseValueList(legacyText).map((value) =>
prefix ? `${prefix}:${value}` : value,
);
}
function uniqueDomainTextValues(values) {
const seen = new Set();
return values.filter((value) => {
const key = `${value}`.toLowerCase();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function loadCombinedDomainText(section_id) {
const textValue = uci.get(UCI_PACKAGE, section_id, "domain_suffix_text");
const values = uniqueDomainTextValues([
...(textValue ? main.parseValueList(textValue) : []),
...domainValuesWithPrefix(section_id, "domain_suffix", ""),
...domainValuesWithPrefix(section_id, "domain", "full"),
...domainValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainValuesWithPrefix(section_id, "domain_regex", "regex"),
...domainTextValuesWithPrefix(section_id, "domain_suffix", ""),
...domainTextValuesWithPrefix(section_id, "domain", "full"),
...domainTextValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainTextValuesWithPrefix(section_id, "domain_regex", "regex"),
]);
return valuesToText(values);
}
function analyzeIpCidrText(value) { function analyzeIpCidrText(value) {
return analyzeTextListValue( return analyzeTextListValue(
value, value,
@@ -3109,6 +3208,10 @@ function addTextConditionField(section, config) {
configureTextareaOption(o, config.textAnalyze); configureTextareaOption(o, config.textAnalyze);
o.load = function (section_id) { o.load = function (section_id) {
if (typeof config.loadText === "function") {
return config.loadText(section_id);
}
const textValue = uci.get(UCI_PACKAGE, section_id, `${config.key}_text`); const textValue = uci.get(UCI_PACKAGE, section_id, `${config.key}_text`);
if (textValue) { if (textValue) {
return textValue; return textValue;
@@ -3128,6 +3231,10 @@ function addTextConditionField(section, config) {
uci.unset(UCI_PACKAGE, section_id, config.key); uci.unset(UCI_PACKAGE, section_id, config.key);
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`); uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
if (typeof config.afterWrite === "function") {
config.afterWrite(section_id);
}
}; };
} }
@@ -4234,8 +4341,26 @@ function createSectionContent(section) {
addTextConditionField(section, { addTextConditionField(section, {
key: "domain_suffix", key: "domain_suffix",
label: _("Domains"), label: _("Domains"),
description: _("Match domains including all subdomains"), description: _(
"Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions.",
),
textAnalyze: analyzeDomainSuffixText, textAnalyze: analyzeDomainSuffixText,
loadText: loadCombinedDomainText,
afterWrite: function (section_id) {
[
"domain",
"domain_keyword",
"domain_regex",
"domain_text",
"domain_keyword_text",
"domain_regex_text",
"domain_text_mode",
"domain_keyword_text_mode",
"domain_regex_text_mode",
].forEach((key) => {
uci.unset(UCI_PACKAGE, section_id, key);
});
},
}); });
addTextConditionField(section, { addTextConditionField(section, {
@@ -4245,34 +4370,6 @@ function createSectionContent(section) {
textAnalyze: analyzeIpCidrText, textAnalyze: analyzeIpCidrText,
}); });
addDynamicConditionField(section, {
key: "domain",
label: _("Exact full domain"),
description: _("Match only one exact full domain name"),
dynamicValidate: function (_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const validation = main.validateDomain(value);
return validation.valid ? true : validation.message;
},
});
addDynamicConditionField(section, {
key: "domain_keyword",
label: _("Domain keyword"),
description: _("Match domains containing a substring"),
dynamicValidate: validateKeyword,
});
addDynamicConditionField(section, {
key: "domain_regex",
label: _("Domain regex"),
description: _("Match domains using a regular expression"),
dynamicValidate: validateRegex,
});
addLocalDeviceSubnetDynamicField(section, { addLocalDeviceSubnetDynamicField(section, {
key: "source_ip_cidr", key: "source_ip_cidr",
label: _("Source IPs"), label: _("Source IPs"),
+25 -31
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PODKOP PLUS\n" "Project-Id-Version: PODKOP PLUS\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-10 22:19+0300\n" "POT-Creation-Date: 2026-06-18 06:38+0300\n"
"PO-Revision-Date: 2026-06-10 22:19+0300\n" "PO-Revision-Date: 2026-06-18 06:38+0300\n"
"Last-Translator: ushan0v\n" "Last-Translator: ushan0v\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -77,6 +77,9 @@ msgstr "Все, кроме выбранных"
msgid "All servers" msgid "All servers"
msgstr "Все серверы" msgstr "Все серверы"
msgid "Allowed domain prefixes are full:, keyword:, and regex:"
msgstr "Разрешённые префиксы доменов: full:, keyword: и regex:"
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
@@ -182,9 +185,6 @@ msgstr "Не удалось сохранить сервер"
msgid "Cascade connection" msgid "Cascade connection"
msgstr "Каскадное подключение" msgstr "Каскадное подключение"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
msgid "Cascade connection cannot use the current section" msgid "Cascade connection cannot use the current section"
msgstr "Каскадное подключение не может использовать текущую секцию" msgstr "Каскадное подключение не может использовать текущую секцию"
@@ -368,12 +368,6 @@ msgstr "Не резолвится"
msgid "Domain and IP Lists" msgid "Domain and IP Lists"
msgstr "Списки доменов и IPs" msgstr "Списки доменов и IPs"
msgid "Domain keyword"
msgstr "Ключевое слово домена"
msgid "Domain regex"
msgstr "Регулярное выражение домена"
msgid "Domain Resolver" msgid "Domain Resolver"
msgstr "Резолвер доменов" msgstr "Резолвер доменов"
@@ -446,9 +440,6 @@ msgstr "Введите полный объект outbound sing-box"
msgid "Enter the subscription URL" msgid "Enter the subscription URL"
msgstr "Введите URL подписки" msgstr "Введите URL подписки"
msgid "Exact full domain"
msgstr "Точный полный домен"
msgid "Exclude country from URLTest" msgid "Exclude country from URLTest"
msgstr "Исключить страну из URLTest" msgstr "Исключить страну из URLTest"
@@ -551,6 +542,9 @@ msgstr "Таймаут рукопожатия"
msgid "Hide filtered servers" msgid "Hide filtered servers"
msgstr "Скрывать отфильтрованные серверы" msgstr "Скрывать отфильтрованные серверы"
msgid "Hide values"
msgstr "Скрывать значения"
msgid "Host" msgid "Host"
msgstr "Хост" msgstr "Хост"
@@ -794,8 +788,8 @@ msgstr "JSON outbound"
msgid "Key path" msgid "Key path"
msgstr "Путь к ключу" msgstr "Путь к ключу"
msgid "Keyword must not contain spaces" msgid "Keyword must not contain spaces or commas"
msgstr "Ключевое слово не должно содержать пробелы" msgstr "Ключевое слово не должно содержать пробелы или запятые"
msgid "Latency test failed" msgid "Latency test failed"
msgstr "" msgstr ""
@@ -845,18 +839,6 @@ msgstr "Совпадение по IP-адресам или подсетям на
msgid "Match destination ports. Use a single port or a range" msgid "Match destination ports. Use a single port or a range"
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон" msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
msgid "Match domains containing a substring"
msgstr "Совпадение по доменам, содержащим подстроку"
msgid "Match domains including all subdomains"
msgstr "Совпадение по доменам, включая все их поддомены"
msgid "Match domains using a regular expression"
msgstr "Совпадение по доменам с использованием регулярного выражения"
msgid "Match only one exact full domain name"
msgstr "Совпадение только по одному точному полному доменному имени"
msgid "Match source IPs or subnets" msgid "Match source IPs or subnets"
msgstr "Совпадение по исходным IP-адресам или подсетям" msgstr "Совпадение по исходным IP-адресам или подсетям"
@@ -1034,6 +1016,9 @@ msgstr "Приостановить обновление"
msgid "Pending" msgid "Pending"
msgstr "Ожидает запуска" msgstr "Ожидает запуска"
msgid "Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions."
msgstr "Обычные значения работают как суффиксы доменов. Используйте full:, keyword: или regex: для точных доменов, ключевых слов и регулярных выражений."
msgid "Podkop Plus Error" msgid "Podkop Plus Error"
msgstr "Ошибка Podkop Plus" msgstr "Ошибка Podkop Plus"
@@ -1148,6 +1133,9 @@ msgstr "Сброс"
msgid "Regional options cannot be used together" msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно" msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Regular expression must not contain spaces or commas"
msgstr "Регулярное выражение не должно содержать пробелы или запятые"
msgid "Remove" msgid "Remove"
msgstr "Удалить" msgstr "Удалить"
@@ -1256,6 +1244,9 @@ msgstr "Выберите предустановленный набор прав
msgid "Select a specific section for downloading external resources" msgid "Select a specific section for downloading external resources"
msgstr "Выберите конкретную секцию для скачки внешних ресурсов" msgstr "Выберите конкретную секцию для скачки внешних ресурсов"
msgid "Select a transit section"
msgstr "Выберите секцию-транзит"
msgid "Select an enabled proxy, JSON outbound, or VPN section" msgid "Select an enabled proxy, JSON outbound, or VPN section"
msgstr "Выберите включенную секцию proxy, JSON outbound или VPN" msgstr "Выберите включенную секцию proxy, JSON outbound или VPN"
@@ -1670,6 +1661,9 @@ msgstr "Укажите IPv4-адрес"
msgid "Use an IPv4 listen address" msgid "Use an IPv4 listen address"
msgstr "Укажите IPv4-адрес прослушивания" msgstr "Укажите IPv4-адрес прослушивания"
msgid "Use another section as an intermediate hop for connecting to this one"
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
msgid "Use CIDR prefixes like 192.168.1.0/24" msgid "Use CIDR prefixes like 192.168.1.0/24"
msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24" msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24"
@@ -1685,9 +1679,6 @@ msgstr "Используйте буквы, цифры, точки, подчёр
msgid "Use sing-box duration format like 1d, 12h or 30m" msgid "Use sing-box duration format like 1d, 12h or 30m"
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m" msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
msgid "Select a transit section"
msgstr "Выберите секцию-транзит"
msgid "Used by" msgid "Used by"
msgstr "Используется" msgstr "Используется"
@@ -1703,6 +1694,9 @@ msgstr "Валидно"
msgid "Validation errors:" msgid "Validation errors:"
msgstr "Ошибки валидации:" msgstr "Ошибки валидации:"
msgid "Value cannot be empty"
msgstr "Значение не может быть пустым"
msgid "Value must not contain control characters" msgid "Value must not contain control characters"
msgstr "Значение не должно содержать управляющие символы" msgstr "Значение не должно содержать управляющие символы"
File diff suppressed because it is too large Load Diff