Add section-level dashboard server filtering
Replace per-group dashboard hiding with a global section filter, including live URLTest and Priority group selectors. Keep server selectors usable before choices exist and preserve shared rule-set values across conditional LuCI fields.
This commit is contained in:
+512
-375
File diff suppressed because it is too large
Load Diff
+451
-368
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: FORKOP\n"
|
"Project-Id-Version: FORKOP\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-13 03:22+0300\n"
|
"POT-Creation-Date: 2026-07-13 00:06+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-13 03:22+0300\n"
|
"PO-Revision-Date: 2026-07-13 00:06+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 "Добавьте URL или локальные пути к списка
|
|||||||
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
||||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
||||||
|
|
||||||
|
msgid "Add URLTest / Priority group"
|
||||||
|
msgstr "Добавить URLTest / Приоритет группу"
|
||||||
|
|
||||||
msgid "Additional marking rules found"
|
msgid "Additional marking rules found"
|
||||||
msgstr "Найдены дополнительные правила маркировки"
|
msgstr "Найдены дополнительные правила маркировки"
|
||||||
|
|
||||||
@@ -86,6 +89,12 @@ msgstr "Дополнительно исключать серверы по про
|
|||||||
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||||
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
|
msgid "Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters."
|
||||||
|
msgstr "Дополнительно скрывать серверы по протоколу, транспорту и защите. Скрывать серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
|
msgid "Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||||
|
msgstr "Дополнительно показывать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
msgid "Address"
|
msgid "Address"
|
||||||
msgstr "Адрес"
|
msgstr "Адрес"
|
||||||
|
|
||||||
@@ -647,6 +656,9 @@ msgstr "Исключить из этого уровня серверы с одн
|
|||||||
msgid "Exclude servers with one of the selected transports from this level."
|
msgid "Exclude servers with one of the selected transports from this level."
|
||||||
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
|
msgid "Exclude URLTest / Priority group"
|
||||||
|
msgstr "Исключить URLTest / Приоритет группу"
|
||||||
|
|
||||||
msgid "Expecting: %s"
|
msgid "Expecting: %s"
|
||||||
msgstr "Ожидается: %s"
|
msgstr "Ожидается: %s"
|
||||||
|
|
||||||
@@ -698,6 +710,9 @@ msgstr "Интервал поиска более быстрого сервера
|
|||||||
msgid "Fastest"
|
msgid "Fastest"
|
||||||
msgstr "Самый быстрый"
|
msgstr "Самый быстрый"
|
||||||
|
|
||||||
|
msgid "Filter the servers that will be displayed on the dashboard."
|
||||||
|
msgstr "Отфильтровать серверы, которые будут отображаться в дашборде."
|
||||||
|
|
||||||
msgid "Firewall WAN port"
|
msgid "Firewall WAN port"
|
||||||
msgstr "Порт в firewall для WAN"
|
msgstr "Порт в firewall для WAN"
|
||||||
|
|
||||||
@@ -794,24 +809,36 @@ msgstr "gRPC-сервис"
|
|||||||
msgid "Handshake timeout"
|
msgid "Handshake timeout"
|
||||||
msgstr "Таймаут рукопожатия"
|
msgstr "Таймаут рукопожатия"
|
||||||
|
|
||||||
msgid "Hide added servers"
|
|
||||||
msgstr "Скрывать добавленные серверы"
|
|
||||||
|
|
||||||
msgid "Hide cascade connection nodes"
|
msgid "Hide cascade connection nodes"
|
||||||
msgstr "Скрывать узлы каскадных подключений"
|
msgstr "Скрывать узлы каскадных подключений"
|
||||||
|
|
||||||
msgid "Hide dashboard servers added to Priority"
|
|
||||||
msgstr "Скрывать серверы, добавленные в Priority, из дашборда"
|
|
||||||
|
|
||||||
msgid "Hide dashboard servers added to URLTest"
|
|
||||||
msgstr "Скрывать серверы из дашборда, которые добавлены в URLTest"
|
|
||||||
|
|
||||||
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
||||||
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
||||||
|
|
||||||
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
||||||
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
||||||
|
|
||||||
|
msgid "Hide selected servers."
|
||||||
|
msgstr "Скрывать выбранные серверы."
|
||||||
|
|
||||||
|
msgid "Hide servers from selected groups without repeating their filtering criteria."
|
||||||
|
msgstr "Скрывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||||
|
|
||||||
|
msgid "Hide servers from the specified countries."
|
||||||
|
msgstr "Скрывать серверы из указанных стран."
|
||||||
|
|
||||||
|
msgid "Hide servers whose names match the expression."
|
||||||
|
msgstr "Скрывать серверы, имена которых совпадают с выражением."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected protocols."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных протоколов."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected security types."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных типов защиты."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected transports."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
msgid "Hide URLTest group nodes"
|
msgid "Hide URLTest group nodes"
|
||||||
msgstr "Скрывать узлы URLTest групп"
|
msgstr "Скрывать узлы URLTest групп"
|
||||||
|
|
||||||
@@ -1646,6 +1673,9 @@ msgstr "Выберите транзитную секцию"
|
|||||||
msgid "Select an existing network interface"
|
msgid "Select an existing network interface"
|
||||||
msgstr "Выберите существующий сетевой интерфейс"
|
msgstr "Выберите существующий сетевой интерфейс"
|
||||||
|
|
||||||
|
msgid "Select an existing URLTest or Priority group"
|
||||||
|
msgstr "Выберите существующую URLTest или Приоритет группу"
|
||||||
|
|
||||||
msgid "Select an intermediate section"
|
msgid "Select an intermediate section"
|
||||||
msgstr "Выберите промежуточную секцию"
|
msgstr "Выберите промежуточную секцию"
|
||||||
|
|
||||||
@@ -1721,6 +1751,9 @@ msgstr "server_port должен быть числом от 1 до 65535"
|
|||||||
msgid "Servers"
|
msgid "Servers"
|
||||||
msgstr "Серверы"
|
msgstr "Серверы"
|
||||||
|
|
||||||
|
msgid "Servers on dashboard"
|
||||||
|
msgstr "Серверы в дашборде"
|
||||||
|
|
||||||
msgid "Service action failed"
|
msgid "Service action failed"
|
||||||
msgstr "Действие сервиса не выполнено"
|
msgstr "Действие сервиса не выполнено"
|
||||||
|
|
||||||
@@ -1736,6 +1769,27 @@ msgstr "Настройки"
|
|||||||
msgid "Show metadata on dashboard"
|
msgid "Show metadata on dashboard"
|
||||||
msgstr "Отображать метаданные в дашборде"
|
msgstr "Отображать метаданные в дашборде"
|
||||||
|
|
||||||
|
msgid "Show only selected servers."
|
||||||
|
msgstr "Показывать только выбранные серверы."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected protocols."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных протоколов."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected security types."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных типов защиты."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected transports."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
|
msgid "Show servers from selected groups without repeating their filtering criteria."
|
||||||
|
msgstr "Показывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||||
|
|
||||||
|
msgid "Show servers only from the specified countries."
|
||||||
|
msgstr "Показывать серверы только из указанных стран."
|
||||||
|
|
||||||
|
msgid "Show servers whose names match the expression."
|
||||||
|
msgstr "Показывать серверы, имена которых совпадают с выражением."
|
||||||
|
|
||||||
msgid "Show sing-box config"
|
msgid "Show sing-box config"
|
||||||
msgstr "Показать sing-box конфигурацию"
|
msgstr "Показать sing-box конфигурацию"
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ type PriorityCacheGroup = {
|
|||||||
fastest_check_interval?: string;
|
fastest_check_interval?: string;
|
||||||
interrupt_exist_connections?: boolean;
|
interrupt_exist_connections?: boolean;
|
||||||
pin_dashboard?: boolean;
|
pin_dashboard?: boolean;
|
||||||
hide_added_outbounds?: boolean;
|
|
||||||
outbounds?: string[];
|
outbounds?: string[];
|
||||||
levels?: PriorityCacheLevel[];
|
levels?: PriorityCacheLevel[];
|
||||||
};
|
};
|
||||||
@@ -100,7 +99,6 @@ type UrlTestConfig = {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
settings: ItemSettings;
|
settings: ItemSettings;
|
||||||
pinDashboard: boolean;
|
pinDashboard: boolean;
|
||||||
hideAddedOutbounds: boolean;
|
|
||||||
showDetectedCountries: boolean;
|
showDetectedCountries: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +108,6 @@ type PriorityConfig = {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
settings: ItemSettings;
|
settings: ItemSettings;
|
||||||
pinDashboard: boolean;
|
pinDashboard: boolean;
|
||||||
hideAddedOutbounds: boolean;
|
|
||||||
healthUrl: string;
|
healthUrl: string;
|
||||||
activeCheckInterval: string;
|
activeCheckInterval: string;
|
||||||
checkTimeout: string;
|
checkTimeout: string;
|
||||||
@@ -286,7 +283,6 @@ function hydrateConfigSections(configSections: Forkop.ConfigSection[]) {
|
|||||||
idle_timeout: item.idle_timeout,
|
idle_timeout: item.idle_timeout,
|
||||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||||
pin_dashboard: item.pin_dashboard,
|
pin_dashboard: item.pin_dashboard,
|
||||||
hide_added_outbounds: item.hide_added_outbounds,
|
|
||||||
urltest_filter_mode: item.filter_mode,
|
urltest_filter_mode: item.filter_mode,
|
||||||
detect_server_country: item.detect_server_country,
|
detect_server_country: item.detect_server_country,
|
||||||
urltest_include_countries: item.include_countries,
|
urltest_include_countries: item.include_countries,
|
||||||
@@ -339,7 +335,6 @@ function hydrateConfigSections(configSections: Forkop.ConfigSection[]) {
|
|||||||
fastest_check_interval: item.fastest_check_interval,
|
fastest_check_interval: item.fastest_check_interval,
|
||||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||||
pin_dashboard: item.pin_dashboard,
|
pin_dashboard: item.pin_dashboard,
|
||||||
hide_added_outbounds: item.hide_added_outbounds,
|
|
||||||
levels,
|
levels,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -661,11 +656,6 @@ function getUrlTestConfigs(section: Forkop.ConfigSection): UrlTestConfig[] {
|
|||||||
displayName: getUrlTestDisplayName(section, id, settings),
|
displayName: getUrlTestDisplayName(section, id, settings),
|
||||||
settings,
|
settings,
|
||||||
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
||||||
hideAddedOutbounds: itemSettingBoolean(
|
|
||||||
settings,
|
|
||||||
'hide_added_outbounds',
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
showDetectedCountries:
|
showDetectedCountries:
|
||||||
filteringEnabled &&
|
filteringEnabled &&
|
||||||
itemSettingString(settings, 'detect_server_country', 'flag_emoji') ===
|
itemSettingString(settings, 'detect_server_country', 'flag_emoji') ===
|
||||||
@@ -748,11 +738,6 @@ function getPriorityConfigs(section: Forkop.ConfigSection): PriorityConfig[] {
|
|||||||
displayName: itemSettingString(settings, 'name', id),
|
displayName: itemSettingString(settings, 'name', id),
|
||||||
settings,
|
settings,
|
||||||
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
pinDashboard: itemSettingBoolean(settings, 'pin_dashboard', true),
|
||||||
hideAddedOutbounds: itemSettingBoolean(
|
|
||||||
settings,
|
|
||||||
'hide_added_outbounds',
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
healthUrl: itemSettingString(
|
healthUrl: itemSettingString(
|
||||||
settings,
|
settings,
|
||||||
'health_url',
|
'health_url',
|
||||||
@@ -1108,31 +1093,6 @@ function buildProxyGroupOutbounds(
|
|||||||
const selectorCodes = selector?.value?.all ?? [];
|
const selectorCodes = selector?.value?.all ?? [];
|
||||||
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
||||||
const priorityCodes = priorityConfigs.map((config) => config.code);
|
const priorityCodes = priorityConfigs.map((config) => config.code);
|
||||||
const urlTestCodeSet = new Set(urlTestCodes);
|
|
||||||
const priorityCodeSet = new Set(priorityCodes);
|
|
||||||
const hideAddedCodeSet = new Set<string>();
|
|
||||||
urlTestEntries.forEach(({ config, entry }) => {
|
|
||||||
if (!config.hideAddedOutbounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childCodes = urltestGroups[config.code]?.outbounds?.length
|
|
||||||
? urltestGroups[config.code].outbounds || []
|
|
||||||
: entry?.value.all || [];
|
|
||||||
|
|
||||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
|
||||||
});
|
|
||||||
priorityEntries.forEach(({ config, entry }) => {
|
|
||||||
if (!config.hideAddedOutbounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childCodes = priorityGroups[config.code]?.outbounds?.length
|
|
||||||
? priorityGroups[config.code].outbounds || []
|
|
||||||
: entry?.value.all || [];
|
|
||||||
|
|
||||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
|
||||||
});
|
|
||||||
const showDetectedCountries =
|
const showDetectedCountries =
|
||||||
urlTestConfigs.some((config) => config.showDetectedCountries) ||
|
urlTestConfigs.some((config) => config.showDetectedCountries) ||
|
||||||
priorityConfigs.some((config) => config.showDetectedCountries);
|
priorityConfigs.some((config) => config.showDetectedCountries);
|
||||||
@@ -1147,17 +1107,7 @@ function buildProxyGroupOutbounds(
|
|||||||
...(selectorCodes.length ? selectorCodes : fallbackCodes),
|
...(selectorCodes.length ? selectorCodes : fallbackCodes),
|
||||||
...urlTestCodes,
|
...urlTestCodes,
|
||||||
...priorityCodes,
|
...priorityCodes,
|
||||||
]).filter((code) => {
|
]);
|
||||||
if (!hideAddedCodeSet.has(code)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
urlTestCodeSet.has(code) ||
|
|
||||||
priorityCodeSet.has(code) ||
|
|
||||||
isUrlTestProxyEntry(proxyByCode.get(code))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
||||||
const item = proxyByCode.get(code);
|
const item = proxyByCode.get(code);
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ function priorityGroup(
|
|||||||
fastest_check_interval: '3m',
|
fastest_check_interval: '3m',
|
||||||
interrupt_exist_connections: '1',
|
interrupt_exist_connections: '1',
|
||||||
pin_dashboard: '1',
|
pin_dashboard: '1',
|
||||||
hide_added_outbounds: '0',
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -423,59 +422,6 @@ describe('getDashboardSections', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides servers already added to the Priority group when enabled', async () => {
|
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
|
||||||
proxySection({ urltests: [], urltest_settings: undefined }),
|
|
||||||
priorityGroup('pg_main', { hide_added_outbounds: '1' }),
|
|
||||||
priorityLevel('pl_de'),
|
|
||||||
]);
|
|
||||||
mocks.getClashApiProxies.mockResolvedValue({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
proxies: {
|
|
||||||
...clashProxies,
|
|
||||||
'main-out': proxy('Selector', {
|
|
||||||
name: 'main-out',
|
|
||||||
now: 'main-priority-pg_main-out',
|
|
||||||
all: ['main-1-out', 'main-2-out', 'main-priority-pg_main-out'],
|
|
||||||
}),
|
|
||||||
'main-priority-pg_main-out': proxy('Selector', {
|
|
||||||
name: 'main-priority-pg_main-out',
|
|
||||||
now: 'main-1-out',
|
|
||||||
all: ['main-1-out', 'main-2-out'],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mocks.fsRead.mockResolvedValue(
|
|
||||||
JSON.stringify({
|
|
||||||
priorityGroups: {
|
|
||||||
'main-priority-pg_main-out': {
|
|
||||||
displayName: 'Codex Priority',
|
|
||||||
outbounds: ['main-1-out', 'main-2-out'],
|
|
||||||
levels: [
|
|
||||||
{
|
|
||||||
id: 'pl_de',
|
|
||||||
displayName: 'Germany',
|
|
||||||
order: 0,
|
|
||||||
outbounds: ['main-1-out', 'main-2-out'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await getDashboardSections();
|
|
||||||
const [section] = result.data;
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
|
||||||
'main-priority-pg_main-out',
|
|
||||||
]);
|
|
||||||
expect(section.latencyTestCodes).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps an empty Priority group visible when sing-box skipped the selector', async () => {
|
it('keeps an empty Priority group visible when sing-box skipped the selector', async () => {
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
mocks.getConfigSections.mockResolvedValue([
|
||||||
proxySection({
|
proxySection({
|
||||||
@@ -522,31 +468,6 @@ describe('getDashboardSections', () => {
|
|||||||
expect(priority?.priorityInfo?.outbounds).toEqual([]);
|
expect(priority?.priorityInfo?.outbounds).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides servers already added to the URLTest group when enabled', async () => {
|
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
|
||||||
proxySection({
|
|
||||||
urltest_settings: urlTestSettings('urltest', {
|
|
||||||
display_name: 'Fastest',
|
|
||||||
hide_added_outbounds: '1',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await getDashboardSections();
|
|
||||||
const [section] = result.data;
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(section.latencyTestCode).toBe('main-out');
|
|
||||||
expect(section.latencyTestCodes).toEqual([
|
|
||||||
'main-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
]);
|
|
||||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
|
||||||
'main-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps legacy URLTest sections compatible before migration', async () => {
|
it('keeps legacy URLTest sections compatible before migration', async () => {
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
mocks.getConfigSections.mockResolvedValue([
|
||||||
proxySection({
|
proxySection({
|
||||||
@@ -636,31 +557,6 @@ describe('getDashboardSections', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(['exclude', 'include', 'mixed', 'disabled'] as const)(
|
|
||||||
'hides added URLTest members independently from %s filter mode',
|
|
||||||
async (urltest_filter_mode) => {
|
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
|
||||||
proxySection({
|
|
||||||
urltest_settings: urlTestSettings('urltest', {
|
|
||||||
display_name: 'Fastest',
|
|
||||||
urltest_filter_mode,
|
|
||||||
hide_added_outbounds: '1',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await getDashboardSections();
|
|
||||||
const [section] = result.data;
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(section.latencyTestCode).toBe('main-out');
|
|
||||||
expect(section.latencyTestCodes).toEqual([
|
|
||||||
'main-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it('keeps non-built-in URLTest groups in selector order when latency sorting is disabled', async () => {
|
it('keeps non-built-in URLTest groups in selector order when latency sorting is disabled', async () => {
|
||||||
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
||||||
mocks.getClashApiProxies.mockResolvedValue({
|
mocks.getClashApiProxies.mockResolvedValue({
|
||||||
@@ -822,72 +718,6 @@ describe('getDashboardSections', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps visible URLTest groups when added leaf servers are hidden', async () => {
|
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
|
||||||
proxySection({
|
|
||||||
urltest_settings: urlTestSettings('urltest', {
|
|
||||||
display_name: 'Fastest',
|
|
||||||
hide_added_outbounds: '1',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
mocks.getClashApiProxies.mockResolvedValue({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
proxies: {
|
|
||||||
'main-out': proxy('Selector', {
|
|
||||||
name: 'main-out',
|
|
||||||
now: 'main-1-out',
|
|
||||||
all: [
|
|
||||||
'main-1-out',
|
|
||||||
'main-provider-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
'main-3-out',
|
|
||||||
'main-urltest-out',
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
'main-urltest-out': proxy('URLTest', {
|
|
||||||
name: 'main-urltest-out',
|
|
||||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 10 }],
|
|
||||||
all: ['main-1-out', 'main-3-out'],
|
|
||||||
}),
|
|
||||||
'main-provider-urltest-out': proxy('URLTest', {
|
|
||||||
name: 'Provider URLTest',
|
|
||||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 150 }],
|
|
||||||
all: ['provider-hidden-1-out'],
|
|
||||||
}),
|
|
||||||
'main-1-out': proxy('VLESS', {
|
|
||||||
name: 'Included 1',
|
|
||||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 100 }],
|
|
||||||
}),
|
|
||||||
'main-2-out': proxy('VLESS', {
|
|
||||||
name: 'Filtered 2',
|
|
||||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 200 }],
|
|
||||||
}),
|
|
||||||
'main-3-out': proxy('VLESS', {
|
|
||||||
name: 'Included 3',
|
|
||||||
history: [{ time: '2026-06-11T00:00:00Z', delay: 300 }],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getDashboardSections();
|
|
||||||
const [section] = result.data;
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(section.latencyTestCodes).toEqual([
|
|
||||||
'main-urltest-out',
|
|
||||||
'main-provider-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
]);
|
|
||||||
expect(section.outbounds.map((item) => item.code)).toEqual([
|
|
||||||
'main-urltest-out',
|
|
||||||
'main-provider-urltest-out',
|
|
||||||
'main-2-out',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not expose flag-emoji detected countries on dashboard outbounds', async () => {
|
it('does not expose flag-emoji detected countries on dashboard outbounds', async () => {
|
||||||
mocks.getConfigSections.mockResolvedValue([
|
mocks.getConfigSections.mockResolvedValue([
|
||||||
proxySection({
|
proxySection({
|
||||||
|
|||||||
@@ -267,6 +267,24 @@ export namespace Forkop {
|
|||||||
urltest_settings?: string;
|
urltest_settings?: string;
|
||||||
priority_groups?: string[];
|
priority_groups?: string[];
|
||||||
priority_group_settings?: string;
|
priority_group_settings?: string;
|
||||||
|
dashboard_filter_mode?: 'disabled' | 'exclude' | 'include' | 'mixed';
|
||||||
|
dashboard_detect_server_country?: 'flag_emoji' | 'country_is';
|
||||||
|
dashboard_include_countries?: string[];
|
||||||
|
dashboard_include_outbounds?: string[];
|
||||||
|
dashboard_include_regex?: string[];
|
||||||
|
dashboard_include_proxy_parameters?: '0' | '1';
|
||||||
|
dashboard_include_protocols?: string[];
|
||||||
|
dashboard_include_transports?: string[];
|
||||||
|
dashboard_include_securities?: string[];
|
||||||
|
dashboard_include_groups?: string[];
|
||||||
|
dashboard_exclude_countries?: string[];
|
||||||
|
dashboard_exclude_outbounds?: string[];
|
||||||
|
dashboard_exclude_regex?: string[];
|
||||||
|
dashboard_exclude_proxy_parameters?: '0' | '1';
|
||||||
|
dashboard_exclude_protocols?: string[];
|
||||||
|
dashboard_exclude_transports?: string[];
|
||||||
|
dashboard_exclude_securities?: string[];
|
||||||
|
dashboard_exclude_groups?: string[];
|
||||||
urltest_proxy_links?: string[];
|
urltest_proxy_links?: string[];
|
||||||
subscription_url?: string;
|
subscription_url?: string;
|
||||||
subscription_user_agent?: string;
|
subscription_user_agent?: string;
|
||||||
@@ -298,7 +316,6 @@ export namespace Forkop {
|
|||||||
idle_timeout?: string;
|
idle_timeout?: string;
|
||||||
interrupt_exist_connections?: '0' | '1';
|
interrupt_exist_connections?: '0' | '1';
|
||||||
pin_dashboard?: '0' | '1';
|
pin_dashboard?: '0' | '1';
|
||||||
hide_added_outbounds?: '0' | '1';
|
|
||||||
filter_mode?: 'disabled' | 'exclude' | 'include' | 'mixed';
|
filter_mode?: 'disabled' | 'exclude' | 'include' | 'mixed';
|
||||||
include_countries?: string[];
|
include_countries?: string[];
|
||||||
include_outbounds?: string[];
|
include_outbounds?: string[];
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ config settings 'settings'
|
|||||||
# list community_lists 'russia_inside'
|
# list community_lists 'russia_inside'
|
||||||
# list rule_set 'https://example.com/rules.srs'
|
# list rule_set 'https://example.com/rules.srs'
|
||||||
# list rule_set_with_subnets 'https://example.com/rules-with-subnets.json'
|
# list rule_set_with_subnets 'https://example.com/rules-with-subnets.json'
|
||||||
|
# option dashboard_filter_mode 'disabled'
|
||||||
|
# option dashboard_detect_server_country 'flag_emoji'
|
||||||
|
# #list dashboard_include_outbounds 'Example server name'
|
||||||
|
# #list dashboard_include_groups 'Fastest'
|
||||||
|
# #list dashboard_exclude_outbounds 'Backup server name'
|
||||||
|
# #list dashboard_exclude_groups 'Backup'
|
||||||
# option outbound_detour_enabled '0'
|
# option outbound_detour_enabled '0'
|
||||||
# #option outbound_detour_section 'main'
|
# #option outbound_detour_section 'main'
|
||||||
# option sort_by_latency '0'
|
# option sort_by_latency '0'
|
||||||
@@ -81,7 +87,6 @@ config settings 'settings'
|
|||||||
# option idle_timeout '30m'
|
# option idle_timeout '30m'
|
||||||
# option interrupt_exist_connections '1'
|
# option interrupt_exist_connections '1'
|
||||||
# option pin_dashboard '1'
|
# option pin_dashboard '1'
|
||||||
# option hide_added_outbounds '0'
|
|
||||||
# option filter_mode 'disabled'
|
# option filter_mode 'disabled'
|
||||||
# option detect_server_country 'flag_emoji'
|
# option detect_server_country 'flag_emoji'
|
||||||
# list exclude_countries 'US'
|
# list exclude_countries 'US'
|
||||||
|
|||||||
@@ -363,6 +363,78 @@ function priority_groups(section) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashboard_filter_mode(section) {
|
||||||
|
return option(section, "dashboard_filter_mode", "disabled") || "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_detect_server_country(section) {
|
||||||
|
return option(section, "dashboard_detect_server_country", "flag_emoji") || "flag_emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_countries(section) {
|
||||||
|
return list_value(section, "dashboard_include_countries");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_outbounds(section) {
|
||||||
|
return list_value(section, "dashboard_include_outbounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_regex(section) {
|
||||||
|
return list_value(section, "dashboard_include_regex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_proxy_parameters(section) {
|
||||||
|
return bool_option(section, "dashboard_include_proxy_parameters", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_protocols(section) {
|
||||||
|
return list_value(section, "dashboard_include_protocols");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_transports(section) {
|
||||||
|
return list_value(section, "dashboard_include_transports");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_securities(section) {
|
||||||
|
return list_value(section, "dashboard_include_securities");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_include_groups(section) {
|
||||||
|
return list_value(section, "dashboard_include_groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_countries(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_countries");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_outbounds(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_outbounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_regex(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_regex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_proxy_parameters(section) {
|
||||||
|
return bool_option(section, "dashboard_exclude_proxy_parameters", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_protocols(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_protocols");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_transports(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_transports");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_securities(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_securities");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_exclude_groups(section) {
|
||||||
|
return list_value(section, "dashboard_exclude_groups");
|
||||||
|
}
|
||||||
|
|
||||||
function priority_group_child(section, value) {
|
function priority_group_child(section, value) {
|
||||||
return owned_child_section(section, "priority_group", value);
|
return owned_child_section(section, "priority_group", value);
|
||||||
}
|
}
|
||||||
@@ -677,13 +749,6 @@ function urltest_detect_server_country(section, value) {
|
|||||||
option(section, "detect_server_country", "flag_emoji") || "flag_emoji");
|
option(section, "detect_server_country", "flag_emoji") || "flag_emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
function urltest_hide_added_outbounds(section, value) {
|
|
||||||
let child = urltest_child(section, value);
|
|
||||||
if (child != null)
|
|
||||||
return child_bool(child, "hide_added_outbounds", false);
|
|
||||||
return item_bool(section, "urltest_settings", value, "hide_added_outbounds", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function urltest_pin_dashboard(section, value) {
|
function urltest_pin_dashboard(section, value) {
|
||||||
let child = urltest_child(section, value);
|
let child = urltest_child(section, value);
|
||||||
if (child != null)
|
if (child != null)
|
||||||
@@ -824,11 +889,6 @@ function priority_group_interrupt_exist_connections(section, value) {
|
|||||||
return child != null ? child_bool(child, "interrupt_exist_connections", true) : true;
|
return child != null ? child_bool(child, "interrupt_exist_connections", true) : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function priority_group_hide_added_outbounds(section, value) {
|
|
||||||
let child = priority_group_child(section, value);
|
|
||||||
return child != null ? child_bool(child, "hide_added_outbounds", false) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function priority_group_pin_dashboard(section, value) {
|
function priority_group_pin_dashboard(section, value) {
|
||||||
let child = priority_group_child(section, value);
|
let child = priority_group_child(section, value);
|
||||||
return child != null ? child_bool(child, "pin_dashboard", true) : true;
|
return child != null ? child_bool(child, "pin_dashboard", true) : true;
|
||||||
@@ -999,6 +1059,24 @@ return {
|
|||||||
interfaces,
|
interfaces,
|
||||||
outbound_jsons,
|
outbound_jsons,
|
||||||
urltests,
|
urltests,
|
||||||
|
dashboard_filter_mode,
|
||||||
|
dashboard_detect_server_country,
|
||||||
|
dashboard_include_countries,
|
||||||
|
dashboard_include_outbounds,
|
||||||
|
dashboard_include_regex,
|
||||||
|
dashboard_include_proxy_parameters,
|
||||||
|
dashboard_include_protocols,
|
||||||
|
dashboard_include_transports,
|
||||||
|
dashboard_include_securities,
|
||||||
|
dashboard_include_groups,
|
||||||
|
dashboard_exclude_countries,
|
||||||
|
dashboard_exclude_outbounds,
|
||||||
|
dashboard_exclude_regex,
|
||||||
|
dashboard_exclude_proxy_parameters,
|
||||||
|
dashboard_exclude_protocols,
|
||||||
|
dashboard_exclude_transports,
|
||||||
|
dashboard_exclude_securities,
|
||||||
|
dashboard_exclude_groups,
|
||||||
community_lists,
|
community_lists,
|
||||||
rule_sets,
|
rule_sets,
|
||||||
rule_sets_with_subnets,
|
rule_sets_with_subnets,
|
||||||
@@ -1033,7 +1111,6 @@ return {
|
|||||||
urltest_interrupt_exist_connections,
|
urltest_interrupt_exist_connections,
|
||||||
urltest_filter_mode,
|
urltest_filter_mode,
|
||||||
urltest_detect_server_country,
|
urltest_detect_server_country,
|
||||||
urltest_hide_added_outbounds,
|
|
||||||
urltest_pin_dashboard,
|
urltest_pin_dashboard,
|
||||||
urltest_include_countries,
|
urltest_include_countries,
|
||||||
urltest_include_outbounds,
|
urltest_include_outbounds,
|
||||||
@@ -1061,7 +1138,6 @@ return {
|
|||||||
priority_group_switch_to_faster_same_priority,
|
priority_group_switch_to_faster_same_priority,
|
||||||
priority_group_fastest_check_interval,
|
priority_group_fastest_check_interval,
|
||||||
priority_group_interrupt_exist_connections,
|
priority_group_interrupt_exist_connections,
|
||||||
priority_group_hide_added_outbounds,
|
|
||||||
priority_group_pin_dashboard,
|
priority_group_pin_dashboard,
|
||||||
priority_level_display_name,
|
priority_level_display_name,
|
||||||
priority_level_order,
|
priority_level_order,
|
||||||
|
|||||||
@@ -1033,6 +1033,13 @@ function validate_urltest_filter_mode_value(value, section) {
|
|||||||
fail_validation("Invalid URLTest filter mode '" + value + "' in rule '" + section + "'. Aborted.");
|
fail_validation("Invalid URLTest filter mode '" + value + "' in rule '" + section + "'. Aborted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate_dashboard_filter_mode_value(value, section) {
|
||||||
|
if (as_string(value) == "" || contains([ "disabled", "exclude", "include", "mixed" ], value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
fail_validation("Invalid dashboard filter mode '" + value + "' in rule '" + section + "'. Aborted.");
|
||||||
|
}
|
||||||
|
|
||||||
function validate_priority_filter_mode_value(value, section, group_id, level_id) {
|
function validate_priority_filter_mode_value(value, section, group_id, level_id) {
|
||||||
if (contains([ "disabled", "exclude", "include", "mixed" ], value))
|
if (contains([ "disabled", "exclude", "include", "mixed" ], value))
|
||||||
return;
|
return;
|
||||||
@@ -1166,6 +1173,56 @@ function validate_priority_group(section, group_id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate_dashboard_group_references(section, values) {
|
||||||
|
let available = {};
|
||||||
|
for (let group_id in connections.urltests(section))
|
||||||
|
available[connections.urltest_display_name(section, group_id)] = true;
|
||||||
|
for (let group_id in connections.priority_groups(section))
|
||||||
|
available[connections.priority_group_display_name(section, group_id)] = true;
|
||||||
|
|
||||||
|
for (let group_name in values)
|
||||||
|
if (!available[group_name])
|
||||||
|
fail_validation("Unknown dashboard URLTest/Priority group '" + group_name + "' in rule '" + section_name(section) + "'. Aborted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate_dashboard_filter(section) {
|
||||||
|
let name = section_name(section);
|
||||||
|
let filter_mode = connections.dashboard_filter_mode(section);
|
||||||
|
validate_dashboard_filter_mode_value(filter_mode, name);
|
||||||
|
if (filter_mode == "disabled")
|
||||||
|
return;
|
||||||
|
|
||||||
|
validate_detect_server_country_value(connections.dashboard_detect_server_country(section), name);
|
||||||
|
if (filter_mode == "include" || filter_mode == "mixed") {
|
||||||
|
for (let value in connections.dashboard_include_countries(section))
|
||||||
|
validate_country_code_value(value, name);
|
||||||
|
for (let value in connections.dashboard_include_regex(section))
|
||||||
|
validate_urltest_regex_value(value, name);
|
||||||
|
validate_dashboard_group_references(section, connections.dashboard_include_groups(section));
|
||||||
|
if (connections.dashboard_include_proxy_parameters(section))
|
||||||
|
validate_proxy_parameter_filters(
|
||||||
|
connections.dashboard_include_protocols(section),
|
||||||
|
connections.dashboard_include_transports(section),
|
||||||
|
connections.dashboard_include_securities(section),
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filter_mode == "exclude" || filter_mode == "mixed") {
|
||||||
|
for (let value in connections.dashboard_exclude_countries(section))
|
||||||
|
validate_country_code_value(value, name);
|
||||||
|
for (let value in connections.dashboard_exclude_regex(section))
|
||||||
|
validate_urltest_regex_value(value, name);
|
||||||
|
validate_dashboard_group_references(section, connections.dashboard_exclude_groups(section));
|
||||||
|
if (connections.dashboard_exclude_proxy_parameters(section))
|
||||||
|
validate_proxy_parameter_filters(
|
||||||
|
connections.dashboard_exclude_protocols(section),
|
||||||
|
connections.dashboard_exclude_transports(section),
|
||||||
|
connections.dashboard_exclude_securities(section),
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validate_combined_domain_value(value, section) {
|
function validate_combined_domain_value(value, section) {
|
||||||
if (as_string(value) == "" || combined_domain_valid(value))
|
if (as_string(value) == "" || combined_domain_valid(value))
|
||||||
return;
|
return;
|
||||||
@@ -1397,6 +1454,8 @@ function validate_rule(section, sections, context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connections.is_connections_action(action)) {
|
if (connections.is_connections_action(action)) {
|
||||||
|
validate_dashboard_filter(section);
|
||||||
|
|
||||||
for (let urltest_id in connections.urltests(section)) {
|
for (let urltest_id in connections.urltests(section)) {
|
||||||
validate_urltest_identifier_value(urltest_id, name);
|
validate_urltest_identifier_value(urltest_id, name);
|
||||||
|
|
||||||
|
|||||||
@@ -959,6 +959,29 @@ function interfaces_signature(section) {
|
|||||||
return sprintf("%J", result);
|
return sprintf("%J", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashboard_filter_signature(section) {
|
||||||
|
return sprintf("%J", {
|
||||||
|
filter_mode: connections.dashboard_filter_mode(section),
|
||||||
|
detect_server_country: connections.dashboard_detect_server_country(section),
|
||||||
|
include_countries: connections.dashboard_include_countries(section),
|
||||||
|
include_outbounds: connections.dashboard_include_outbounds(section),
|
||||||
|
include_regex: connections.dashboard_include_regex(section),
|
||||||
|
include_proxy_parameters: connections.dashboard_include_proxy_parameters(section) ? "1" : "0",
|
||||||
|
include_protocols: connections.dashboard_include_protocols(section),
|
||||||
|
include_transports: connections.dashboard_include_transports(section),
|
||||||
|
include_securities: connections.dashboard_include_securities(section),
|
||||||
|
include_groups: connections.dashboard_include_groups(section),
|
||||||
|
exclude_countries: connections.dashboard_exclude_countries(section),
|
||||||
|
exclude_outbounds: connections.dashboard_exclude_outbounds(section),
|
||||||
|
exclude_regex: connections.dashboard_exclude_regex(section),
|
||||||
|
exclude_proxy_parameters: connections.dashboard_exclude_proxy_parameters(section) ? "1" : "0",
|
||||||
|
exclude_protocols: connections.dashboard_exclude_protocols(section),
|
||||||
|
exclude_transports: connections.dashboard_exclude_transports(section),
|
||||||
|
exclude_securities: connections.dashboard_exclude_securities(section),
|
||||||
|
exclude_groups: connections.dashboard_exclude_groups(section)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function urltests_signature(section) {
|
function urltests_signature(section) {
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let entry in connections.urltests(section)) {
|
for (let entry in connections.urltests(section)) {
|
||||||
@@ -971,7 +994,6 @@ function urltests_signature(section) {
|
|||||||
idle_timeout: connections.urltest_idle_timeout(section, entry),
|
idle_timeout: connections.urltest_idle_timeout(section, entry),
|
||||||
interrupt_exist_connections: connections.urltest_interrupt_exist_connections(section, entry) ? "1" : "0",
|
interrupt_exist_connections: connections.urltest_interrupt_exist_connections(section, entry) ? "1" : "0",
|
||||||
pin_dashboard: connections.urltest_pin_dashboard(section, entry) ? "1" : "0",
|
pin_dashboard: connections.urltest_pin_dashboard(section, entry) ? "1" : "0",
|
||||||
hide_added_outbounds: connections.urltest_hide_added_outbounds(section, entry) ? "1" : "0",
|
|
||||||
filter_mode: connections.urltest_filter_mode(section, entry),
|
filter_mode: connections.urltest_filter_mode(section, entry),
|
||||||
detect_server_country: connections.urltest_detect_server_country(section, entry),
|
detect_server_country: connections.urltest_detect_server_country(section, entry),
|
||||||
include_countries: connections.urltest_include_countries(section, entry),
|
include_countries: connections.urltest_include_countries(section, entry),
|
||||||
@@ -1018,7 +1040,6 @@ function priority_groups_signature(section) {
|
|||||||
fastest_check_interval: connections.priority_group_fastest_check_interval(section, group_id),
|
fastest_check_interval: connections.priority_group_fastest_check_interval(section, group_id),
|
||||||
interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id) ? "1" : "0",
|
interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id) ? "1" : "0",
|
||||||
pin_dashboard: connections.priority_group_pin_dashboard(section, group_id) ? "1" : "0",
|
pin_dashboard: connections.priority_group_pin_dashboard(section, group_id) ? "1" : "0",
|
||||||
hide_added_outbounds: connections.priority_group_hide_added_outbounds(section, group_id) ? "1" : "0",
|
|
||||||
levels
|
levels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1205,6 +1226,7 @@ function append_sing_box_rule_signature_body(body, section, sections) {
|
|||||||
body = signature_add_value(body, prefix + ".legacy_outbound_json", option(section, "outbound_json", ""));
|
body = signature_add_value(body, prefix + ".legacy_outbound_json", option(section, "outbound_json", ""));
|
||||||
body = signature_add_value(body, prefix + ".urltests", urltests_signature(section));
|
body = signature_add_value(body, prefix + ".urltests", urltests_signature(section));
|
||||||
body = signature_add_value(body, prefix + ".priority_groups", priority_groups_signature(section));
|
body = signature_add_value(body, prefix + ".priority_groups", priority_groups_signature(section));
|
||||||
|
body = signature_add_value(body, prefix + ".dashboard_filter", dashboard_filter_signature(section));
|
||||||
body = signature_add_value(body, prefix + ".urltest_enabled", bool_option_value(section, "urltest_enabled", false));
|
body = signature_add_value(body, prefix + ".urltest_enabled", bool_option_value(section, "urltest_enabled", false));
|
||||||
body = signature_add_value(body, prefix + ".detect_server_country", normalize_detect_server_country_method(option(section, "detect_server_country", "flag_emoji")));
|
body = signature_add_value(body, prefix + ".detect_server_country", normalize_detect_server_country_method(option(section, "detect_server_country", "flag_emoji")));
|
||||||
body = signature_add_value(body, prefix + ".urltest_check_interval", section_urltest_check_interval(section));
|
body = signature_add_value(body, prefix + ".urltest_check_interval", section_urltest_check_interval(section));
|
||||||
|
|||||||
@@ -895,6 +895,15 @@ function configured_country_filter(mode, include_countries, exclude_countries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function section_needs_country_is(section) {
|
function section_needs_country_is(section) {
|
||||||
|
let dashboard_mode = connections.dashboard_filter_mode(section);
|
||||||
|
if (connections.dashboard_detect_server_country(section) == "country_is" &&
|
||||||
|
configured_country_filter(
|
||||||
|
dashboard_mode,
|
||||||
|
connections.dashboard_include_countries(section),
|
||||||
|
connections.dashboard_exclude_countries(section)
|
||||||
|
))
|
||||||
|
return true;
|
||||||
|
|
||||||
for (let urltest_id in connections.urltests(section)) {
|
for (let urltest_id in connections.urltests(section)) {
|
||||||
let mode = connections.urltest_filter_mode(section, urltest_id);
|
let mode = connections.urltest_filter_mode(section, urltest_id);
|
||||||
if (connections.urltest_detect_server_country(section, urltest_id) == "country_is" &&
|
if (connections.urltest_detect_server_country(section, urltest_id) == "country_is" &&
|
||||||
@@ -1028,30 +1037,32 @@ function urltest_all_candidate_outbounds(urltest_candidate_tags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function urltest_matching_candidate_outbounds(urltest_candidate_tags, names, countries, name_filter, regexes, country_filter,
|
function urltest_matching_candidate_outbounds(urltest_candidate_tags, names, countries, name_filter, regexes, country_filter,
|
||||||
metadata, proxy_parameters_enabled, proxy_parameters_operator, protocols, transports, securities) {
|
metadata, proxy_parameters_enabled, proxy_parameters_operator, protocols, transports, securities, additional_matches) {
|
||||||
names = object_or_empty(names);
|
names = object_or_empty(names);
|
||||||
countries = object_or_empty(countries);
|
countries = object_or_empty(countries);
|
||||||
country_filter = runtime_urltest.normalized_country_list(country_filter);
|
country_filter = runtime_urltest.normalized_country_list(country_filter);
|
||||||
|
|
||||||
let regex_set = regex_match_set(urltest_candidate_tags, names, regexes);
|
let regex_set = regex_match_set(urltest_candidate_tags, names, regexes);
|
||||||
let base_filter_configured = name_or_country_filter_configured(name_filter, regexes, country_filter);
|
let base_filter_configured = name_or_country_filter_configured(name_filter, regexes, country_filter);
|
||||||
|
let additional_set = object_keys_set(additional_matches);
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
for (let tag in array_or_empty(urltest_candidate_tags)) {
|
for (let tag in array_or_empty(urltest_candidate_tags)) {
|
||||||
let base_matches = tag_name_filter_matches(tag, names, name_filter, regex_set) ||
|
let base_matches = tag_name_filter_matches(tag, names, name_filter, regex_set) ||
|
||||||
tag_country_filter_matches(tag, countries, country_filter);
|
tag_country_filter_matches(tag, countries, country_filter);
|
||||||
let matches = base_matches;
|
let matches = additional_set[tag] || base_matches;
|
||||||
if (proxy_parameters_enabled && proxy_parameters_operator == "or") {
|
if (proxy_parameters_enabled && proxy_parameters_operator == "or") {
|
||||||
matches = base_matches || proxy_parameter_filter_matches_any(
|
matches = additional_set[tag] || base_matches || proxy_parameter_filter_matches_any(
|
||||||
tag, metadata, protocols, transports, securities
|
tag, metadata, protocols, transports, securities
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if (proxy_parameters_enabled) {
|
else if (proxy_parameters_enabled) {
|
||||||
if (!base_filter_configured)
|
if (!base_filter_configured)
|
||||||
base_matches = true;
|
base_matches = true;
|
||||||
matches = base_matches && proxy_parameter_filter_matches_all(
|
matches = additional_set[tag] ||
|
||||||
tag, metadata, protocols, transports, securities
|
(base_matches && proxy_parameter_filter_matches_all(
|
||||||
);
|
tag, metadata, protocols, transports, securities
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches)
|
if (matches)
|
||||||
@@ -1075,7 +1086,8 @@ function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names,
|
|||||||
include_names, include_regex, include_countries,
|
include_names, include_regex, include_countries,
|
||||||
include_proxy_parameters, include_protocols, include_transports, include_securities,
|
include_proxy_parameters, include_protocols, include_transports, include_securities,
|
||||||
exclude_names, exclude_regex, exclude_countries,
|
exclude_names, exclude_regex, exclude_countries,
|
||||||
exclude_proxy_parameters, exclude_protocols, exclude_transports, exclude_securities) {
|
exclude_proxy_parameters, exclude_protocols, exclude_transports, exclude_securities,
|
||||||
|
include_additional_matches, exclude_additional_matches) {
|
||||||
let all_outbounds = urltest_all_candidate_outbounds(urltest_candidate_tags);
|
let all_outbounds = urltest_all_candidate_outbounds(urltest_candidate_tags);
|
||||||
if (filter_mode == "" || filter_mode == "disabled")
|
if (filter_mode == "" || filter_mode == "disabled")
|
||||||
return all_outbounds;
|
return all_outbounds;
|
||||||
@@ -1094,7 +1106,8 @@ function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names,
|
|||||||
"and",
|
"and",
|
||||||
include_protocols,
|
include_protocols,
|
||||||
include_transports,
|
include_transports,
|
||||||
include_securities
|
include_securities,
|
||||||
|
include_additional_matches
|
||||||
);
|
);
|
||||||
let exclude_outbounds = urltest_matching_candidate_outbounds(
|
let exclude_outbounds = urltest_matching_candidate_outbounds(
|
||||||
urltest_candidate_tags,
|
urltest_candidate_tags,
|
||||||
@@ -1108,7 +1121,8 @@ function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names,
|
|||||||
"or",
|
"or",
|
||||||
exclude_protocols,
|
exclude_protocols,
|
||||||
exclude_transports,
|
exclude_transports,
|
||||||
exclude_securities
|
exclude_securities,
|
||||||
|
exclude_additional_matches
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filter_mode == "include")
|
if (filter_mode == "include")
|
||||||
@@ -1179,6 +1193,59 @@ function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashboard_country_metadata(section, state) {
|
||||||
|
let metadata = object_or_empty(object_or_empty(state.outboundMetadata).countries);
|
||||||
|
if (connections.dashboard_detect_server_country(section) == "flag_emoji")
|
||||||
|
return runtime_urltest.countries_from_flag_names(object_or_empty(object_or_empty(state.outboundMetadata).names));
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selected_group_outbounds(group_names, group_outbounds) {
|
||||||
|
group_outbounds = object_or_empty(group_outbounds);
|
||||||
|
let result = [];
|
||||||
|
for (let group_name in array_or_empty(group_names))
|
||||||
|
for (let tag_name in array_or_empty(group_outbounds[group_name]))
|
||||||
|
push(result, tag_name);
|
||||||
|
return unique_string_array(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remember_dashboard_group_outbounds(group_outbounds, group_name, outbounds) {
|
||||||
|
group_name = as_string(group_name);
|
||||||
|
if (group_name == "")
|
||||||
|
return;
|
||||||
|
|
||||||
|
let combined = array_or_empty(group_outbounds[group_name]);
|
||||||
|
for (let tag_name in array_or_empty(outbounds))
|
||||||
|
push(combined, tag_name);
|
||||||
|
group_outbounds[group_name] = unique_string_array(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dashboard_filtered_outbounds(section, selector_tags, state, group_outbounds) {
|
||||||
|
return filter_candidate_outbounds(
|
||||||
|
connections.dashboard_filter_mode(section),
|
||||||
|
selector_tags,
|
||||||
|
object_or_empty(object_or_empty(state.outboundMetadata).names),
|
||||||
|
dashboard_country_metadata(section, state),
|
||||||
|
object_or_empty(state.outboundMetadata),
|
||||||
|
connections.dashboard_include_outbounds(section),
|
||||||
|
connections.dashboard_include_regex(section),
|
||||||
|
connections.dashboard_include_countries(section),
|
||||||
|
connections.dashboard_include_proxy_parameters(section),
|
||||||
|
connections.dashboard_include_protocols(section),
|
||||||
|
connections.dashboard_include_transports(section),
|
||||||
|
connections.dashboard_include_securities(section),
|
||||||
|
connections.dashboard_exclude_outbounds(section),
|
||||||
|
connections.dashboard_exclude_regex(section),
|
||||||
|
connections.dashboard_exclude_countries(section),
|
||||||
|
connections.dashboard_exclude_proxy_parameters(section),
|
||||||
|
connections.dashboard_exclude_protocols(section),
|
||||||
|
connections.dashboard_exclude_transports(section),
|
||||||
|
connections.dashboard_exclude_securities(section),
|
||||||
|
selected_group_outbounds(connections.dashboard_include_groups(section), group_outbounds),
|
||||||
|
selected_group_outbounds(connections.dashboard_exclude_groups(section), group_outbounds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function priority_levels_with_outbounds(group_id, urltest_candidate_tags, state) {
|
function priority_levels_with_outbounds(group_id, urltest_candidate_tags, state) {
|
||||||
let result = [];
|
let result = [];
|
||||||
let assigned = {};
|
let assigned = {};
|
||||||
@@ -1303,7 +1370,6 @@ function add_priority_group_outbound(config, section, group_id, urltest_candidat
|
|||||||
fastest_check_interval: connections.priority_group_fastest_check_interval(section, group_id),
|
fastest_check_interval: connections.priority_group_fastest_check_interval(section, group_id),
|
||||||
interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id),
|
interrupt_exist_connections: connections.priority_group_interrupt_exist_connections(section, group_id),
|
||||||
pin_dashboard: connections.priority_group_pin_dashboard(section, group_id),
|
pin_dashboard: connections.priority_group_pin_dashboard(section, group_id),
|
||||||
hide_added_outbounds: connections.priority_group_hide_added_outbounds(section, group_id),
|
|
||||||
outbounds,
|
outbounds,
|
||||||
levels
|
levels
|
||||||
});
|
});
|
||||||
@@ -1328,40 +1394,37 @@ function add_proxy_selector(config, section, selector_tags, urltest_candidate_ta
|
|||||||
let selector_default = selector_tags[0];
|
let selector_default = selector_tags[0];
|
||||||
let urltest_tags = [];
|
let urltest_tags = [];
|
||||||
let priority_tags = [];
|
let priority_tags = [];
|
||||||
let hidden_selector_tags = {};
|
let group_outbounds = {};
|
||||||
|
|
||||||
for (let urltest_id in connections.urltests(section)) {
|
for (let urltest_id in connections.urltests(section)) {
|
||||||
let urltest = add_urltest_outbound(config, section, urltest_id, urltest_candidate_tags, state);
|
let urltest = add_urltest_outbound(config, section, urltest_id, urltest_candidate_tags, state);
|
||||||
|
remember_dashboard_group_outbounds(
|
||||||
|
group_outbounds,
|
||||||
|
connections.urltest_display_name(section, urltest_id),
|
||||||
|
urltest.outbounds
|
||||||
|
);
|
||||||
if (urltest.tag == "")
|
if (urltest.tag == "")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
push(urltest_tags, urltest.tag);
|
push(urltest_tags, urltest.tag);
|
||||||
|
|
||||||
if (connections.urltest_hide_added_outbounds(section, urltest_id)) {
|
|
||||||
for (let tag in array_or_empty(urltest.outbounds))
|
|
||||||
hidden_selector_tags[tag] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let group_id in connections.priority_groups(section)) {
|
for (let group_id in connections.priority_groups(section)) {
|
||||||
let priority = add_priority_group_outbound(config, section, group_id, urltest_candidate_tags, state);
|
let priority = add_priority_group_outbound(config, section, group_id, urltest_candidate_tags, state);
|
||||||
|
remember_dashboard_group_outbounds(
|
||||||
|
group_outbounds,
|
||||||
|
connections.priority_group_display_name(section, group_id),
|
||||||
|
priority.outbounds
|
||||||
|
);
|
||||||
if (priority.tag == "")
|
if (priority.tag == "")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
push(priority_tags, priority.tag);
|
push(priority_tags, priority.tag);
|
||||||
|
|
||||||
if (connections.priority_group_hide_added_outbounds(section, group_id)) {
|
|
||||||
for (let tag in array_or_empty(priority.outbounds))
|
|
||||||
hidden_selector_tags[tag] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selector_outbounds = dashboard_filtered_outbounds(section, selector_tags, state, group_outbounds);
|
||||||
|
selector_default = selector_outbounds[0];
|
||||||
if (length(urltest_tags) > 0 || length(priority_tags) > 0) {
|
if (length(urltest_tags) > 0 || length(priority_tags) > 0) {
|
||||||
selector_outbounds = [];
|
|
||||||
for (let tag in selector_tags) {
|
|
||||||
if (!hidden_selector_tags[tag])
|
|
||||||
push(selector_outbounds, tag);
|
|
||||||
}
|
|
||||||
for (let tag in urltest_tags)
|
for (let tag in urltest_tags)
|
||||||
push(selector_outbounds, tag);
|
push(selector_outbounds, tag);
|
||||||
for (let tag in priority_tags)
|
for (let tag in priority_tags)
|
||||||
@@ -1369,6 +1432,9 @@ function add_proxy_selector(config, section, selector_tags, urltest_candidate_ta
|
|||||||
selector_default = length(urltest_tags) > 0 ? urltest_tags[0] : priority_tags[0];
|
selector_default = length(urltest_tags) > 0 ? urltest_tags[0] : priority_tags[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (length(selector_outbounds) == 0)
|
||||||
|
runtime_generate_unsupported("dashboard server filtering produced no usable outbounds");
|
||||||
|
|
||||||
push(config.outbounds, {
|
push(config.outbounds, {
|
||||||
type: "selector",
|
type: "selector",
|
||||||
tag: selector_tag,
|
tag: selector_tag,
|
||||||
|
|||||||
@@ -3309,7 +3309,6 @@ function hydrateConfigSections(configSections) {
|
|||||||
idle_timeout: item.idle_timeout,
|
idle_timeout: item.idle_timeout,
|
||||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||||
pin_dashboard: item.pin_dashboard,
|
pin_dashboard: item.pin_dashboard,
|
||||||
hide_added_outbounds: item.hide_added_outbounds,
|
|
||||||
urltest_filter_mode: item.filter_mode,
|
urltest_filter_mode: item.filter_mode,
|
||||||
detect_server_country: item.detect_server_country,
|
detect_server_country: item.detect_server_country,
|
||||||
urltest_include_countries: item.include_countries,
|
urltest_include_countries: item.include_countries,
|
||||||
@@ -3356,7 +3355,6 @@ function hydrateConfigSections(configSections) {
|
|||||||
fastest_check_interval: item.fastest_check_interval,
|
fastest_check_interval: item.fastest_check_interval,
|
||||||
interrupt_exist_connections: item.interrupt_exist_connections,
|
interrupt_exist_connections: item.interrupt_exist_connections,
|
||||||
pin_dashboard: item.pin_dashboard,
|
pin_dashboard: item.pin_dashboard,
|
||||||
hide_added_outbounds: item.hide_added_outbounds,
|
|
||||||
levels
|
levels
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -3572,11 +3570,6 @@ function getUrlTestConfigs(section) {
|
|||||||
displayName: getUrlTestDisplayName(section, id, settings),
|
displayName: getUrlTestDisplayName(section, id, settings),
|
||||||
settings,
|
settings,
|
||||||
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
||||||
hideAddedOutbounds: itemSettingBoolean(
|
|
||||||
settings,
|
|
||||||
"hide_added_outbounds",
|
|
||||||
false
|
|
||||||
),
|
|
||||||
showDetectedCountries: filteringEnabled && itemSettingString(settings, "detect_server_country", "flag_emoji") === "country_is"
|
showDetectedCountries: filteringEnabled && itemSettingString(settings, "detect_server_country", "flag_emoji") === "country_is"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -3635,11 +3628,6 @@ function getPriorityConfigs(section) {
|
|||||||
displayName: itemSettingString(settings, "name", id),
|
displayName: itemSettingString(settings, "name", id),
|
||||||
settings,
|
settings,
|
||||||
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true),
|
||||||
hideAddedOutbounds: itemSettingBoolean(
|
|
||||||
settings,
|
|
||||||
"hide_added_outbounds",
|
|
||||||
false
|
|
||||||
),
|
|
||||||
healthUrl: itemSettingString(
|
healthUrl: itemSettingString(
|
||||||
settings,
|
settings,
|
||||||
"health_url",
|
"health_url",
|
||||||
@@ -3884,23 +3872,6 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
|||||||
const selectorCodes = selector?.value?.all ?? [];
|
const selectorCodes = selector?.value?.all ?? [];
|
||||||
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
const urlTestCodes = urlTestConfigs.map((config) => config.code);
|
||||||
const priorityCodes = priorityConfigs.map((config) => config.code);
|
const priorityCodes = priorityConfigs.map((config) => config.code);
|
||||||
const urlTestCodeSet = new Set(urlTestCodes);
|
|
||||||
const priorityCodeSet = new Set(priorityCodes);
|
|
||||||
const hideAddedCodeSet = /* @__PURE__ */ new Set();
|
|
||||||
urlTestEntries.forEach(({ config, entry }) => {
|
|
||||||
if (!config.hideAddedOutbounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const childCodes = urltestGroups[config.code]?.outbounds?.length ? urltestGroups[config.code].outbounds || [] : entry?.value.all || [];
|
|
||||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
|
||||||
});
|
|
||||||
priorityEntries.forEach(({ config, entry }) => {
|
|
||||||
if (!config.hideAddedOutbounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const childCodes = priorityGroups[config.code]?.outbounds?.length ? priorityGroups[config.code].outbounds || [] : entry?.value.all || [];
|
|
||||||
childCodes.forEach((code) => hideAddedCodeSet.add(code));
|
|
||||||
});
|
|
||||||
const showDetectedCountries = urlTestConfigs.some((config) => config.showDetectedCountries) || priorityConfigs.some((config) => config.showDetectedCountries);
|
const showDetectedCountries = urlTestConfigs.some((config) => config.showDetectedCountries) || priorityConfigs.some((config) => config.showDetectedCountries);
|
||||||
const builtInUrltestCode = urlTestCodes[0] || "";
|
const builtInUrltestCode = urlTestCodes[0] || "";
|
||||||
const fallbackCodes = uniqueCodes([
|
const fallbackCodes = uniqueCodes([
|
||||||
@@ -3913,12 +3884,7 @@ function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGro
|
|||||||
...selectorCodes.length ? selectorCodes : fallbackCodes,
|
...selectorCodes.length ? selectorCodes : fallbackCodes,
|
||||||
...urlTestCodes,
|
...urlTestCodes,
|
||||||
...priorityCodes
|
...priorityCodes
|
||||||
]).filter((code) => {
|
]);
|
||||||
if (!hideAddedCodeSet.has(code)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return urlTestCodeSet.has(code) || priorityCodeSet.has(code) || isUrlTestProxyEntry(proxyByCode.get(code));
|
|
||||||
});
|
|
||||||
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
const outbounds = uniqueCodes(groupCodes).flatMap((code) => {
|
||||||
const item = proxyByCode.get(code);
|
const item = proxyByCode.get(code);
|
||||||
const urlTestConfig = urlTestConfigByCode.get(code);
|
const urlTestConfig = urlTestConfigByCode.get(code);
|
||||||
|
|||||||
@@ -431,6 +431,8 @@ let actionProvidersAvailabilityLoader = null;
|
|||||||
const outboundNameChoicesCache = new Map();
|
const outboundNameChoicesCache = new Map();
|
||||||
const outboundNameChoicesInflight = new Map();
|
const outboundNameChoicesInflight = new Map();
|
||||||
const outboundNameSourceOptions = new Map();
|
const outboundNameSourceOptions = new Map();
|
||||||
|
const sectionGroupSourceOptions = new Map();
|
||||||
|
const dashboardFilterChoiceRefreshers = new Map();
|
||||||
const SECTION_CACHE_DIR = "/var/run/forkop/section-cache";
|
const SECTION_CACHE_DIR = "/var/run/forkop/section-cache";
|
||||||
const COUNTRY_CODES =
|
const COUNTRY_CODES =
|
||||||
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW XK".split(
|
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW XK".split(
|
||||||
@@ -1323,7 +1325,14 @@ const SettingsDynamicList = form.DynamicList.extend({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return widget.render();
|
const node = widget.render();
|
||||||
|
if (typeof this.onListChange === "function") {
|
||||||
|
node.addEventListener("cbi-dynlist-change", () => {
|
||||||
|
this.onListChange(section_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
},
|
},
|
||||||
|
|
||||||
parse(section_id) {
|
parse(section_id) {
|
||||||
@@ -1415,6 +1424,78 @@ const ButtonAddSettingsDynamicList = SettingsDynamicList.extend({
|
|||||||
buttonAdd: true,
|
buttonAdd: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function configureLiveDynamicListChoices(option, getChoices) {
|
||||||
|
option.renderWidget = function (section_id, _option_index, cfgvalue) {
|
||||||
|
const values = L.toArray(cfgvalue != null ? cfgvalue : this.default);
|
||||||
|
const choices = getChoices(section_id, values);
|
||||||
|
const labels = {};
|
||||||
|
choices.forEach((choice) => {
|
||||||
|
labels[choice.value] = choice.label;
|
||||||
|
});
|
||||||
|
refreshOptionChoices(this, choices);
|
||||||
|
let choiceSignature = JSON.stringify(
|
||||||
|
choices.map((choice) => [choice.value, choice.label]),
|
||||||
|
);
|
||||||
|
// form.DynamicList passes null when there are no choices, which permanently
|
||||||
|
// renders a plain input. An empty object keeps the stock combobox alive so
|
||||||
|
// later live choices can be added without reopening the modal.
|
||||||
|
const widget = new ui.DynamicList(values, labels, {
|
||||||
|
id: this.cbid(section_id),
|
||||||
|
sort: this.keylist,
|
||||||
|
allowduplicates: this.allowduplicates,
|
||||||
|
optional: this.optional || this.rmempty,
|
||||||
|
datatype: this.datatype,
|
||||||
|
placeholder: this.placeholder,
|
||||||
|
validate: L.bind(this.validate, this, section_id),
|
||||||
|
disabled: this.readonly != null ? this.readonly : this.map.readonly,
|
||||||
|
});
|
||||||
|
const node = widget.render();
|
||||||
|
const refreshChoices = () => {
|
||||||
|
if (!node.isConnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValues = widget.getValue();
|
||||||
|
const currentChoices = getChoices(section_id, currentValues);
|
||||||
|
const currentLabels = {};
|
||||||
|
currentChoices.forEach((choice) => {
|
||||||
|
currentLabels[choice.value] = choice.label;
|
||||||
|
});
|
||||||
|
const currentSignature = JSON.stringify(
|
||||||
|
currentChoices.map((choice) => [choice.value, choice.label]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentSignature === choiceSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
choiceSignature = currentSignature;
|
||||||
|
|
||||||
|
refreshOptionChoices(this, currentChoices);
|
||||||
|
widget.choices = currentLabels;
|
||||||
|
widget.clearChoices();
|
||||||
|
widget.addChoices(
|
||||||
|
currentChoices.map((choice) => choice.value),
|
||||||
|
currentLabels,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const refreshBeforeOpening = (event) => {
|
||||||
|
if (event.target && event.target.closest(".add-item")) {
|
||||||
|
refreshChoices();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
node.addEventListener("mousedown", refreshBeforeOpening, true);
|
||||||
|
node.addEventListener("focusin", refreshBeforeOpening, true);
|
||||||
|
|
||||||
|
if (!dashboardFilterChoiceRefreshers.has(section_id)) {
|
||||||
|
dashboardFilterChoiceRefreshers.set(section_id, new Set());
|
||||||
|
}
|
||||||
|
dashboardFilterChoiceRefreshers.get(section_id).add(refreshChoices);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function countryChoices() {
|
function countryChoices() {
|
||||||
return COUNTRY_CODES.map((code) => ({
|
return COUNTRY_CODES.map((code) => ({
|
||||||
value: code,
|
value: code,
|
||||||
@@ -1529,6 +1610,94 @@ function currentDraftOutboundNames(section_id) {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentSectionGroupValues(section_id, typeName) {
|
||||||
|
const liveValues = currentLiveDynamicListValues(section_id, typeName);
|
||||||
|
if (liveValues != null) {
|
||||||
|
return liveValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = sectionGroupSourceOptions.get(typeName);
|
||||||
|
if (option && typeof option.formvalue === "function") {
|
||||||
|
try {
|
||||||
|
const value = option.formvalue(section_id);
|
||||||
|
if (value != null) {
|
||||||
|
return normalizeDynamicListItems(value);
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Fall back to the child sections when the widget is not mounted.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getChildItemIds(section_id, typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionGroupDisplayName(section_id, typeName, value) {
|
||||||
|
const itemId = `${value || ""}`.trim();
|
||||||
|
if (isExistingChildItem(section_id, itemId, typeName)) {
|
||||||
|
return `${uci.get(UCI_PACKAGE, itemId, "name") || itemId}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = sectionGroupSourceOptions.get(typeName);
|
||||||
|
const pending =
|
||||||
|
option && option.pendingChildSettings
|
||||||
|
? option.pendingChildSettings[section_id]
|
||||||
|
: null;
|
||||||
|
const settings = pending && pending[itemId];
|
||||||
|
return `${(settings && settings.name) || itemId}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentSectionGroupChoices(section_id, selectedValues = []) {
|
||||||
|
const seen = new Set();
|
||||||
|
const result = [];
|
||||||
|
const append = (typeName, value) => {
|
||||||
|
value = `${value || ""}`.trim();
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = sectionGroupDisplayName(section_id, typeName, value) || value;
|
||||||
|
if (seen.has(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(name);
|
||||||
|
result.push({ value: name, label: name });
|
||||||
|
};
|
||||||
|
|
||||||
|
currentSectionGroupValues(section_id, "urltest").forEach((value) =>
|
||||||
|
append("urltest", value),
|
||||||
|
);
|
||||||
|
currentSectionGroupValues(section_id, "priority_group").forEach((value) =>
|
||||||
|
append("priority_group", value),
|
||||||
|
);
|
||||||
|
normalizeDynamicListItems(selectedValues).forEach((value) => {
|
||||||
|
value = `${value || ""}`.trim();
|
||||||
|
if (value && !seen.has(value)) {
|
||||||
|
seen.add(value);
|
||||||
|
result.push({ value, label: value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.sort((left, right) => left.label.localeCompare(right.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDashboardFilterChoiceWidgets(section_id) {
|
||||||
|
const refreshers = dashboardFilterChoiceRefreshers.get(section_id);
|
||||||
|
if (!refreshers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshers.forEach((refresh) => {
|
||||||
|
if (refresh() === false) {
|
||||||
|
refreshers.delete(refresh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refreshers.size === 0) {
|
||||||
|
dashboardFilterChoiceRefreshers.delete(section_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isDownloadThroughTargetSection(section, currentSectionId) {
|
function isDownloadThroughTargetSection(section, currentSectionId) {
|
||||||
const sectionName = getUciSectionName(section);
|
const sectionName = getUciSectionName(section);
|
||||||
const action = (section && section.action) || "";
|
const action = (section && section.action) || "";
|
||||||
@@ -1916,7 +2085,6 @@ function urlTestSettingsKeys() {
|
|||||||
"idle_timeout",
|
"idle_timeout",
|
||||||
"interrupt_exist_connections",
|
"interrupt_exist_connections",
|
||||||
"pin_dashboard",
|
"pin_dashboard",
|
||||||
"hide_added_outbounds",
|
|
||||||
"filter_mode",
|
"filter_mode",
|
||||||
"detect_server_country",
|
"detect_server_country",
|
||||||
"include_countries",
|
"include_countries",
|
||||||
@@ -1945,7 +2113,6 @@ function defaultUrlTestSettings(name) {
|
|||||||
idle_timeout: "30m",
|
idle_timeout: "30m",
|
||||||
interrupt_exist_connections: "1",
|
interrupt_exist_connections: "1",
|
||||||
pin_dashboard: "1",
|
pin_dashboard: "1",
|
||||||
hide_added_outbounds: "0",
|
|
||||||
filter_mode: "disabled",
|
filter_mode: "disabled",
|
||||||
detect_server_country: "flag_emoji",
|
detect_server_country: "flag_emoji",
|
||||||
};
|
};
|
||||||
@@ -1959,7 +2126,6 @@ function urlTestChildDefaults() {
|
|||||||
idle_timeout: "30m",
|
idle_timeout: "30m",
|
||||||
interrupt_exist_connections: "1",
|
interrupt_exist_connections: "1",
|
||||||
pin_dashboard: "1",
|
pin_dashboard: "1",
|
||||||
hide_added_outbounds: "0",
|
|
||||||
filter_mode: "disabled",
|
filter_mode: "disabled",
|
||||||
detect_server_country: "flag_emoji",
|
detect_server_country: "flag_emoji",
|
||||||
};
|
};
|
||||||
@@ -1977,7 +2143,6 @@ function priorityGroupSettingsKeys() {
|
|||||||
"fastest_check_interval",
|
"fastest_check_interval",
|
||||||
"interrupt_exist_connections",
|
"interrupt_exist_connections",
|
||||||
"pin_dashboard",
|
"pin_dashboard",
|
||||||
"hide_added_outbounds",
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1993,7 +2158,6 @@ function defaultPriorityGroupSettings() {
|
|||||||
fastest_check_interval: "3m",
|
fastest_check_interval: "3m",
|
||||||
interrupt_exist_connections: "1",
|
interrupt_exist_connections: "1",
|
||||||
pin_dashboard: "1",
|
pin_dashboard: "1",
|
||||||
hide_added_outbounds: "0",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2008,7 +2172,6 @@ function priorityGroupChildDefaults() {
|
|||||||
fastest_check_interval: "3m",
|
fastest_check_interval: "3m",
|
||||||
interrupt_exist_connections: "1",
|
interrupt_exist_connections: "1",
|
||||||
pin_dashboard: "1",
|
pin_dashboard: "1",
|
||||||
hide_added_outbounds: "0",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2368,15 +2531,6 @@ function addUrlTestItemOptions(itemSection, options = {}) {
|
|||||||
o.default = "1";
|
o.default = "1";
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|
||||||
o = itemSection.option(
|
|
||||||
form.Flag,
|
|
||||||
"hide_added_outbounds",
|
|
||||||
_("Hide added servers"),
|
|
||||||
_("Hide dashboard servers added to URLTest"),
|
|
||||||
);
|
|
||||||
o.default = "0";
|
|
||||||
o.rmempty = false;
|
|
||||||
|
|
||||||
o = itemSection.option(
|
o = itemSection.option(
|
||||||
form.ListValue,
|
form.ListValue,
|
||||||
"filter_mode",
|
"filter_mode",
|
||||||
@@ -2507,6 +2661,9 @@ function addUrlTestItemOptions(itemSection, options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
list.placeholder = _("-- Select --");
|
list.placeholder = _("-- Select --");
|
||||||
|
configureLiveDynamicListChoices(list, (itemId, values) =>
|
||||||
|
currentOutboundNameChoices(parentSectionForItem(itemId), values),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (validator) {
|
if (validator) {
|
||||||
list.validate = function (_itemId, value) {
|
list.validate = function (_itemId, value) {
|
||||||
@@ -2717,6 +2874,9 @@ function addPriorityLevelItemOptions(itemSection, options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
list.placeholder = _("-- Select --");
|
list.placeholder = _("-- Select --");
|
||||||
|
configureLiveDynamicListChoices(list, (itemId, values) =>
|
||||||
|
currentOutboundNameChoices(parentSectionForItem(itemId), values),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (validator) {
|
if (validator) {
|
||||||
list.validate = function (_itemId, value) {
|
list.validate = function (_itemId, value) {
|
||||||
@@ -2857,15 +3017,6 @@ function addPriorityGroupItemOptions(itemSection, options = {}) {
|
|||||||
o.default = "1";
|
o.default = "1";
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|
||||||
o = itemSection.option(
|
|
||||||
form.Flag,
|
|
||||||
"hide_added_outbounds",
|
|
||||||
_("Hide added servers"),
|
|
||||||
_("Hide dashboard servers added to Priority"),
|
|
||||||
);
|
|
||||||
o.default = "0";
|
|
||||||
o.rmempty = false;
|
|
||||||
|
|
||||||
o = itemSection.option(
|
o = itemSection.option(
|
||||||
ButtonAddSettingsDynamicList,
|
ButtonAddSettingsDynamicList,
|
||||||
"priority_level",
|
"priority_level",
|
||||||
@@ -2937,6 +3088,229 @@ function addPriorityGroupItemOptions(itemSection, options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addDashboardGroupFilterOption(
|
||||||
|
optionSection,
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
modes,
|
||||||
|
) {
|
||||||
|
const list = optionSection.option(form.DynamicList, key, label, description);
|
||||||
|
modes.forEach((mode) => {
|
||||||
|
list.depends({
|
||||||
|
action: "connection",
|
||||||
|
dashboard_filter_mode: mode,
|
||||||
|
urltest: /.+/,
|
||||||
|
});
|
||||||
|
list.depends({
|
||||||
|
action: "connection",
|
||||||
|
dashboard_filter_mode: mode,
|
||||||
|
priority_group: /.+/,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
list.rmempty = true;
|
||||||
|
list.placeholder = _("-- Select --");
|
||||||
|
list.load = function (section_id) {
|
||||||
|
const values = getConfigListValues(section_id, key);
|
||||||
|
const choices = currentSectionGroupChoices(section_id, values);
|
||||||
|
refreshOptionChoices(this, choices);
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
list.validate = function (section_id, value) {
|
||||||
|
const selected = new Set(
|
||||||
|
currentSectionGroupChoices(section_id).map((choice) => choice.value),
|
||||||
|
);
|
||||||
|
return normalizeDynamicListItems(value).every((item) => selected.has(item))
|
||||||
|
? true
|
||||||
|
: _("Select an existing URLTest or Priority group");
|
||||||
|
};
|
||||||
|
configureLiveDynamicListChoices(list, currentSectionGroupChoices);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDashboardServerFilterOptions(section) {
|
||||||
|
const optionSection = {
|
||||||
|
option: (optionType, ...args) => {
|
||||||
|
const option = section.taboption("settings", optionType, ...args);
|
||||||
|
option.modalonly = true;
|
||||||
|
return option;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let o = optionSection.option(
|
||||||
|
form.ListValue,
|
||||||
|
"dashboard_filter_mode",
|
||||||
|
_("Servers on dashboard"),
|
||||||
|
_("Filter the servers that will be displayed on the dashboard."),
|
||||||
|
);
|
||||||
|
urlTestFilterModeChoices().forEach((choice) =>
|
||||||
|
o.value(choice.value, choice.label),
|
||||||
|
);
|
||||||
|
o.default = "disabled";
|
||||||
|
o.depends("action", "connection");
|
||||||
|
|
||||||
|
o = optionSection.option(
|
||||||
|
form.ListValue,
|
||||||
|
"dashboard_detect_server_country",
|
||||||
|
_("Detect server country"),
|
||||||
|
);
|
||||||
|
["exclude", "include", "mixed"].forEach((mode) =>
|
||||||
|
o.depends({ action: "connection", dashboard_filter_mode: mode }),
|
||||||
|
);
|
||||||
|
serverCountryDetectionChoices().forEach((choice) =>
|
||||||
|
o.value(choice.value, choice.label),
|
||||||
|
);
|
||||||
|
o.default = "flag_emoji";
|
||||||
|
|
||||||
|
const includeProxyParameterOptions = {
|
||||||
|
prefix: "dashboard_include",
|
||||||
|
toggleLabel: _("Include by proxy parameters"),
|
||||||
|
toggleDescription: _(
|
||||||
|
"Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters.",
|
||||||
|
),
|
||||||
|
dependencies: [
|
||||||
|
{ action: "connection", dashboard_filter_mode: "include" },
|
||||||
|
{ action: "connection", dashboard_filter_mode: "mixed" },
|
||||||
|
],
|
||||||
|
protocolDescription: _(
|
||||||
|
"Show only servers with one of the selected protocols.",
|
||||||
|
),
|
||||||
|
transportDescription: _(
|
||||||
|
"Show only servers with one of the selected transports.",
|
||||||
|
),
|
||||||
|
securityDescription: _(
|
||||||
|
"Show only servers with one of the selected security types.",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const excludeProxyParameterOptions = {
|
||||||
|
prefix: "dashboard_exclude",
|
||||||
|
toggleLabel: _("Exclude by proxy parameters"),
|
||||||
|
toggleDescription: _(
|
||||||
|
"Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters.",
|
||||||
|
),
|
||||||
|
dependencies: [
|
||||||
|
{ action: "connection", dashboard_filter_mode: "exclude" },
|
||||||
|
{ action: "connection", dashboard_filter_mode: "mixed" },
|
||||||
|
],
|
||||||
|
protocolDescription: _("Hide servers with one of the selected protocols."),
|
||||||
|
transportDescription: _(
|
||||||
|
"Hide servers with one of the selected transports.",
|
||||||
|
),
|
||||||
|
securityDescription: _(
|
||||||
|
"Hide servers with one of the selected security types.",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"dashboard_include_countries",
|
||||||
|
_("Include countries"),
|
||||||
|
_("Show servers only from the specified countries."),
|
||||||
|
countryChoices(),
|
||||||
|
validateCountryCode,
|
||||||
|
["include", "mixed"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dashboard_include_outbounds",
|
||||||
|
_("Include servers"),
|
||||||
|
_("Show only selected servers."),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
["include", "mixed"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dashboard_include_regex",
|
||||||
|
_("Include by regular expression"),
|
||||||
|
_("Show servers whose names match the expression."),
|
||||||
|
null,
|
||||||
|
validateRegex,
|
||||||
|
["include", "mixed"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dashboard_exclude_countries",
|
||||||
|
_("Exclude countries"),
|
||||||
|
_("Hide servers from the specified countries."),
|
||||||
|
countryChoices(),
|
||||||
|
validateCountryCode,
|
||||||
|
["exclude", "mixed"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dashboard_exclude_outbounds",
|
||||||
|
_("Exclude servers"),
|
||||||
|
_("Hide selected servers."),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
["exclude", "mixed"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dashboard_exclude_regex",
|
||||||
|
_("Exclude by regular expression"),
|
||||||
|
_("Hide servers whose names match the expression."),
|
||||||
|
null,
|
||||||
|
validateRegex,
|
||||||
|
["exclude", "mixed"],
|
||||||
|
],
|
||||||
|
].forEach(([key, label, description, choices, validator, modes]) => {
|
||||||
|
const list = optionSection.option(
|
||||||
|
form.DynamicList,
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
modes.forEach((mode) =>
|
||||||
|
list.depends({ action: "connection", dashboard_filter_mode: mode }),
|
||||||
|
);
|
||||||
|
list.rmempty = true;
|
||||||
|
if (choices) {
|
||||||
|
choices.forEach((choice) => list.value(choice.value, choice.label));
|
||||||
|
list.placeholder = _("-- Select --");
|
||||||
|
}
|
||||||
|
if (key.endsWith("_outbounds")) {
|
||||||
|
list.load = function (section_id) {
|
||||||
|
const values = getConfigListValues(section_id, key);
|
||||||
|
loadOutboundNameChoices(section_id).then(() => {
|
||||||
|
refreshDashboardFilterChoiceWidgets(section_id);
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
};
|
||||||
|
list.placeholder = _("-- Select --");
|
||||||
|
configureLiveDynamicListChoices(list, currentOutboundNameChoices);
|
||||||
|
}
|
||||||
|
if (validator) {
|
||||||
|
list.validate = function (_section_id, value) {
|
||||||
|
return validator(null, value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (key === "dashboard_include_regex") {
|
||||||
|
addDashboardGroupFilterOption(
|
||||||
|
optionSection,
|
||||||
|
"dashboard_include_groups",
|
||||||
|
_("Add URLTest / Priority group"),
|
||||||
|
_(
|
||||||
|
"Show servers from selected groups without repeating their filtering criteria.",
|
||||||
|
),
|
||||||
|
["include", "mixed"],
|
||||||
|
);
|
||||||
|
addProxyParameterFilterOptions(
|
||||||
|
optionSection,
|
||||||
|
includeProxyParameterOptions,
|
||||||
|
);
|
||||||
|
} else if (key === "dashboard_exclude_regex") {
|
||||||
|
addDashboardGroupFilterOption(
|
||||||
|
optionSection,
|
||||||
|
"dashboard_exclude_groups",
|
||||||
|
_("Exclude URLTest / Priority group"),
|
||||||
|
_(
|
||||||
|
"Hide servers from selected groups without repeating their filtering criteria.",
|
||||||
|
),
|
||||||
|
["exclude", "mixed"],
|
||||||
|
);
|
||||||
|
addProxyParameterFilterOptions(
|
||||||
|
optionSection,
|
||||||
|
excludeProxyParameterOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function settingValueEquals(left, right) {
|
function settingValueEquals(left, right) {
|
||||||
const normalize = (value) => {
|
const normalize = (value) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@@ -6855,6 +7229,9 @@ function createSectionContent(section) {
|
|||||||
const validation = main.validateProxyUrl(value);
|
const validation = main.validateProxyUrl(value);
|
||||||
return validation.valid ? true : validation.message;
|
return validation.valid ? true : validation.message;
|
||||||
};
|
};
|
||||||
|
o.onchange = function (_event, section_id) {
|
||||||
|
refreshDashboardFilterChoiceWidgets(section_id);
|
||||||
|
};
|
||||||
outboundNameSourceOptions.set("selector_proxy_links", o);
|
outboundNameSourceOptions.set("selector_proxy_links", o);
|
||||||
|
|
||||||
o = section.taboption(
|
o = section.taboption(
|
||||||
@@ -6943,6 +7320,7 @@ function createSectionContent(section) {
|
|||||||
delete this.pendingChildSettings[section_id];
|
delete this.pendingChildSettings[section_id];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||||
outboundNameSourceOptions.set("interfaces", o);
|
outboundNameSourceOptions.set("interfaces", o);
|
||||||
|
|
||||||
o = section.taboption(
|
o = section.taboption(
|
||||||
@@ -6969,6 +7347,7 @@ function createSectionContent(section) {
|
|||||||
const validation = main.validateOutboundJson(value);
|
const validation = main.validateOutboundJson(value);
|
||||||
return validation.valid ? true : validation.message;
|
return validation.valid ? true : validation.message;
|
||||||
};
|
};
|
||||||
|
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||||
outboundNameSourceOptions.set("outbound_jsons", o);
|
outboundNameSourceOptions.set("outbound_jsons", o);
|
||||||
|
|
||||||
o = section.taboption(
|
o = section.taboption(
|
||||||
@@ -7027,6 +7406,8 @@ function createSectionContent(section) {
|
|||||||
this.inputValueForItem(section_id, itemId),
|
this.inputValueForItem(section_id, itemId),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||||
|
sectionGroupSourceOptions.set("urltest", o);
|
||||||
|
|
||||||
o = section.taboption(
|
o = section.taboption(
|
||||||
"settings",
|
"settings",
|
||||||
@@ -7064,6 +7445,8 @@ function createSectionContent(section) {
|
|||||||
this.inputValueForItem(section_id, itemId),
|
this.inputValueForItem(section_id, itemId),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
o.onListChange = refreshDashboardFilterChoiceWidgets;
|
||||||
|
sectionGroupSourceOptions.set("priority_group", o);
|
||||||
|
|
||||||
o = section.taboption(
|
o = section.taboption(
|
||||||
"settings",
|
"settings",
|
||||||
@@ -7444,6 +7827,8 @@ function createSectionContent(section) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
ruleSetOption.modalonly = true;
|
ruleSetOption.modalonly = true;
|
||||||
|
// Both widgets map to rule_set, so neither inactive view may erase shared storage.
|
||||||
|
ruleSetOption.retain = true;
|
||||||
dependsOnRoutingAction(ruleSetOption);
|
dependsOnRoutingAction(ruleSetOption);
|
||||||
ruleSetOption.renderItemSettingsModal = showRuleSetSettingsModal;
|
ruleSetOption.renderItemSettingsModal = showRuleSetSettingsModal;
|
||||||
ruleSetOption.load = function (section_id) {
|
ruleSetOption.load = function (section_id) {
|
||||||
@@ -7472,6 +7857,7 @@ function createSectionContent(section) {
|
|||||||
);
|
);
|
||||||
dnsRuleSetOption.depends("action", "dns");
|
dnsRuleSetOption.depends("action", "dns");
|
||||||
dnsRuleSetOption.modalonly = true;
|
dnsRuleSetOption.modalonly = true;
|
||||||
|
dnsRuleSetOption.retain = true;
|
||||||
dnsRuleSetOption.load = function (section_id) {
|
dnsRuleSetOption.load = function (section_id) {
|
||||||
return getCustomRulesetReferences(section_id);
|
return getCustomRulesetReferences(section_id);
|
||||||
};
|
};
|
||||||
@@ -7527,6 +7913,8 @@ function createSectionContent(section) {
|
|||||||
dynamicValidate: validatePortCondition,
|
dynamicValidate: validatePortCondition,
|
||||||
});
|
});
|
||||||
dependsOnRoutingAction(portsOption);
|
dependsOnRoutingAction(portsOption);
|
||||||
|
|
||||||
|
addDashboardServerFilterOptions(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSectionTableOptions(sectionRef) {
|
function loadSectionTableOptions(sectionRef) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: FORKOP\n"
|
"Project-Id-Version: FORKOP\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-13 03:22+0300\n"
|
"POT-Creation-Date: 2026-07-13 00:06+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-13 03:22+0300\n"
|
"PO-Revision-Date: 2026-07-13 00:06+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 "Добавьте URL или локальные пути к списка
|
|||||||
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
msgid "Add URLs or local paths to .srs / .json lists. Only domain rules are supported."
|
||||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Поддерживаются только доменные правила."
|
||||||
|
|
||||||
|
msgid "Add URLTest / Priority group"
|
||||||
|
msgstr "Добавить URLTest / Приоритет группу"
|
||||||
|
|
||||||
msgid "Additional marking rules found"
|
msgid "Additional marking rules found"
|
||||||
msgstr "Найдены дополнительные правила маркировки"
|
msgstr "Найдены дополнительные правила маркировки"
|
||||||
|
|
||||||
@@ -86,6 +89,12 @@ msgstr "Дополнительно исключать серверы по про
|
|||||||
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
msgid "Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||||
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
msgstr "Дополнительно отбирать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
|
msgid "Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters."
|
||||||
|
msgstr "Дополнительно скрывать серверы по протоколу, транспорту и защите. Скрывать серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
|
msgid "Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters."
|
||||||
|
msgstr "Дополнительно показывать серверы по протоколу, транспорту и защите. Добавлять только серверы, совпавшие по указанным параметрам."
|
||||||
|
|
||||||
msgid "Address"
|
msgid "Address"
|
||||||
msgstr "Адрес"
|
msgstr "Адрес"
|
||||||
|
|
||||||
@@ -647,6 +656,9 @@ msgstr "Исключить из этого уровня серверы с одн
|
|||||||
msgid "Exclude servers with one of the selected transports from this level."
|
msgid "Exclude servers with one of the selected transports from this level."
|
||||||
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
msgstr "Исключить из этого уровня серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
|
msgid "Exclude URLTest / Priority group"
|
||||||
|
msgstr "Исключить URLTest / Приоритет группу"
|
||||||
|
|
||||||
msgid "Expecting: %s"
|
msgid "Expecting: %s"
|
||||||
msgstr "Ожидается: %s"
|
msgstr "Ожидается: %s"
|
||||||
|
|
||||||
@@ -698,6 +710,9 @@ msgstr "Интервал поиска более быстрого сервера
|
|||||||
msgid "Fastest"
|
msgid "Fastest"
|
||||||
msgstr "Самый быстрый"
|
msgstr "Самый быстрый"
|
||||||
|
|
||||||
|
msgid "Filter the servers that will be displayed on the dashboard."
|
||||||
|
msgstr "Отфильтровать серверы, которые будут отображаться в дашборде."
|
||||||
|
|
||||||
msgid "Firewall WAN port"
|
msgid "Firewall WAN port"
|
||||||
msgstr "Порт в firewall для WAN"
|
msgstr "Порт в firewall для WAN"
|
||||||
|
|
||||||
@@ -794,24 +809,36 @@ msgstr "gRPC-сервис"
|
|||||||
msgid "Handshake timeout"
|
msgid "Handshake timeout"
|
||||||
msgstr "Таймаут рукопожатия"
|
msgstr "Таймаут рукопожатия"
|
||||||
|
|
||||||
msgid "Hide added servers"
|
|
||||||
msgstr "Скрывать добавленные серверы"
|
|
||||||
|
|
||||||
msgid "Hide cascade connection nodes"
|
msgid "Hide cascade connection nodes"
|
||||||
msgstr "Скрывать узлы каскадных подключений"
|
msgstr "Скрывать узлы каскадных подключений"
|
||||||
|
|
||||||
msgid "Hide dashboard servers added to Priority"
|
|
||||||
msgstr "Скрывать серверы, добавленные в Priority, из дашборда"
|
|
||||||
|
|
||||||
msgid "Hide dashboard servers added to URLTest"
|
|
||||||
msgstr "Скрывать серверы из дашборда, которые добавлены в URLTest"
|
|
||||||
|
|
||||||
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
msgid "Hide individual nodes that are already included in imported subscription URLTest groups"
|
||||||
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
msgstr "Скрывать отдельные узлы, которые уже входят в импортированные URLTest группы подписки"
|
||||||
|
|
||||||
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
msgid "Hide intermediate nodes used as detours by other subscription nodes"
|
||||||
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
msgstr "Скрывать промежуточные узлы, которые используются как detour для других узлов подписки"
|
||||||
|
|
||||||
|
msgid "Hide selected servers."
|
||||||
|
msgstr "Скрывать выбранные серверы."
|
||||||
|
|
||||||
|
msgid "Hide servers from selected groups without repeating their filtering criteria."
|
||||||
|
msgstr "Скрывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||||
|
|
||||||
|
msgid "Hide servers from the specified countries."
|
||||||
|
msgstr "Скрывать серверы из указанных стран."
|
||||||
|
|
||||||
|
msgid "Hide servers whose names match the expression."
|
||||||
|
msgstr "Скрывать серверы, имена которых совпадают с выражением."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected protocols."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных протоколов."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected security types."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных типов защиты."
|
||||||
|
|
||||||
|
msgid "Hide servers with one of the selected transports."
|
||||||
|
msgstr "Скрывать серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
msgid "Hide URLTest group nodes"
|
msgid "Hide URLTest group nodes"
|
||||||
msgstr "Скрывать узлы URLTest групп"
|
msgstr "Скрывать узлы URLTest групп"
|
||||||
|
|
||||||
@@ -1646,6 +1673,9 @@ msgstr "Выберите транзитную секцию"
|
|||||||
msgid "Select an existing network interface"
|
msgid "Select an existing network interface"
|
||||||
msgstr "Выберите существующий сетевой интерфейс"
|
msgstr "Выберите существующий сетевой интерфейс"
|
||||||
|
|
||||||
|
msgid "Select an existing URLTest or Priority group"
|
||||||
|
msgstr "Выберите существующую URLTest или Приоритет группу"
|
||||||
|
|
||||||
msgid "Select an intermediate section"
|
msgid "Select an intermediate section"
|
||||||
msgstr "Выберите промежуточную секцию"
|
msgstr "Выберите промежуточную секцию"
|
||||||
|
|
||||||
@@ -1721,6 +1751,9 @@ msgstr "server_port должен быть числом от 1 до 65535"
|
|||||||
msgid "Servers"
|
msgid "Servers"
|
||||||
msgstr "Серверы"
|
msgstr "Серверы"
|
||||||
|
|
||||||
|
msgid "Servers on dashboard"
|
||||||
|
msgstr "Серверы в дашборде"
|
||||||
|
|
||||||
msgid "Service action failed"
|
msgid "Service action failed"
|
||||||
msgstr "Действие сервиса не выполнено"
|
msgstr "Действие сервиса не выполнено"
|
||||||
|
|
||||||
@@ -1736,6 +1769,27 @@ msgstr "Настройки"
|
|||||||
msgid "Show metadata on dashboard"
|
msgid "Show metadata on dashboard"
|
||||||
msgstr "Отображать метаданные в дашборде"
|
msgstr "Отображать метаданные в дашборде"
|
||||||
|
|
||||||
|
msgid "Show only selected servers."
|
||||||
|
msgstr "Показывать только выбранные серверы."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected protocols."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных протоколов."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected security types."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных типов защиты."
|
||||||
|
|
||||||
|
msgid "Show only servers with one of the selected transports."
|
||||||
|
msgstr "Показывать только серверы с одним из выбранных транспортов."
|
||||||
|
|
||||||
|
msgid "Show servers from selected groups without repeating their filtering criteria."
|
||||||
|
msgstr "Показывать серверы из выбранных групп без повторения их параметров фильтрации."
|
||||||
|
|
||||||
|
msgid "Show servers only from the specified countries."
|
||||||
|
msgstr "Показывать серверы только из указанных стран."
|
||||||
|
|
||||||
|
msgid "Show servers whose names match the expression."
|
||||||
|
msgstr "Показывать серверы, имена которых совпадают с выражением."
|
||||||
|
|
||||||
msgid "Show sing-box config"
|
msgid "Show sing-box config"
|
||||||
msgstr "Показать sing-box конфигурацию"
|
msgstr "Показать sing-box конфигурацию"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
FORKOP_LIB="$ROOT_DIR/forkop/files/usr/lib"
|
||||||
|
GENERATOR_UC="$FORKOP_LIB/singbox/generator.uc"
|
||||||
|
VALIDATOR_UC="$FORKOP_LIB/config/validator.uc"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_config() {
|
||||||
|
local fixture="$1"
|
||||||
|
local output="$2"
|
||||||
|
mkdir -p "${output}.section-cache"
|
||||||
|
ucode -L "$FORKOP_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||||
|
"$fixture" "$output" "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_fixture() {
|
||||||
|
local fixture="$1"
|
||||||
|
FORKOP_LIB="$FORKOP_LIB" ucode -L "$FORKOP_LIB" "$VALIDATOR_UC" \
|
||||||
|
validate-runtime-fixture "$fixture" "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cat >"$WORK_DIR/mixed.json" <<'JSON'
|
||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
".name": "settings",
|
||||||
|
".type": "settings",
|
||||||
|
"dns_server": [ "77.88.8.8" ],
|
||||||
|
"bootstrap_dns_server": [ "77.88.8.8" ]
|
||||||
|
},
|
||||||
|
"section": [
|
||||||
|
{
|
||||||
|
".name": "proxy",
|
||||||
|
".type": "section",
|
||||||
|
"enabled": "1",
|
||||||
|
"action": "connection",
|
||||||
|
"outbound_jsons": [
|
||||||
|
"{\"type\":\"vless\",\"tag\":\"Alpha\",\"server\":\"alpha.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000001\",\"tls\":{\"enabled\":true}}",
|
||||||
|
"{\"type\":\"vless\",\"tag\":\"Beta\",\"server\":\"beta.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000002\",\"transport\":{\"type\":\"ws\"},\"tls\":{\"enabled\":true,\"reality\":{\"enabled\":true}}}",
|
||||||
|
"{\"type\":\"vmess\",\"tag\":\"Gamma\",\"server\":\"gamma.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000003\",\"transport\":{\"type\":\"grpc\"}}",
|
||||||
|
"{\"type\":\"trojan\",\"tag\":\"Delta\",\"server\":\"delta.example\",\"server_port\":443,\"password\":\"secret\",\"transport\":{\"type\":\"xhttp\"},\"tls\":{\"enabled\":true}}",
|
||||||
|
"{\"type\":\"http\",\"tag\":\"Echo\",\"server\":\"echo.example\",\"server_port\":8080}"
|
||||||
|
],
|
||||||
|
"dashboard_filter_mode": "mixed",
|
||||||
|
"dashboard_include_proxy_parameters": "1",
|
||||||
|
"dashboard_include_protocols": [ "http" ],
|
||||||
|
"dashboard_include_groups": [ "Shared group" ],
|
||||||
|
"dashboard_exclude_groups": [ "Blocked group" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"urltest": [
|
||||||
|
{
|
||||||
|
".name": "ut_main",
|
||||||
|
".type": "urltest",
|
||||||
|
"section": "proxy",
|
||||||
|
"name": "Shared group",
|
||||||
|
"filter_mode": "include",
|
||||||
|
"include_outbounds": [ "Alpha", "Beta" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"priority_group": [
|
||||||
|
{
|
||||||
|
".name": "pg_shared",
|
||||||
|
".type": "priority_group",
|
||||||
|
"section": "proxy",
|
||||||
|
"name": "Shared group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
".name": "pg_main",
|
||||||
|
".type": "priority_group",
|
||||||
|
"section": "proxy",
|
||||||
|
"name": "Blocked group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"priority_level": [
|
||||||
|
{
|
||||||
|
".name": "pl_shared",
|
||||||
|
".type": "priority_level",
|
||||||
|
"group": "pg_shared",
|
||||||
|
"name": "Shared members",
|
||||||
|
"order": "0",
|
||||||
|
"filter_mode": "include",
|
||||||
|
"include_outbounds": [ "Gamma" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
".name": "pl_main",
|
||||||
|
".type": "priority_level",
|
||||||
|
"group": "pg_main",
|
||||||
|
"name": "Blocked members",
|
||||||
|
"order": "0",
|
||||||
|
"filter_mode": "include",
|
||||||
|
"include_outbounds": [ "Beta", "Gamma" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
node - "$WORK_DIR/mixed.json" "$WORK_DIR" <<'JS'
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const source = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||||
|
const outputDir = process.argv[3];
|
||||||
|
|
||||||
|
for (const mode of ['disabled', 'include', 'exclude']) {
|
||||||
|
const fixture = structuredClone(source);
|
||||||
|
const section = fixture.section[0];
|
||||||
|
section.dashboard_filter_mode = mode;
|
||||||
|
if (mode === 'disabled') {
|
||||||
|
delete section.dashboard_include_proxy_parameters;
|
||||||
|
delete section.dashboard_include_protocols;
|
||||||
|
delete section.dashboard_include_groups;
|
||||||
|
delete section.dashboard_exclude_groups;
|
||||||
|
} else if (mode === 'include') {
|
||||||
|
delete section.dashboard_include_proxy_parameters;
|
||||||
|
delete section.dashboard_include_protocols;
|
||||||
|
delete section.dashboard_exclude_groups;
|
||||||
|
} else {
|
||||||
|
delete section.dashboard_include_proxy_parameters;
|
||||||
|
delete section.dashboard_include_protocols;
|
||||||
|
delete section.dashboard_include_groups;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(outputDir, `${mode}.json`), JSON.stringify(fixture));
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalid = structuredClone(source);
|
||||||
|
invalid.section[0].dashboard_include_groups = ['missing_group'];
|
||||||
|
fs.writeFileSync(path.join(outputDir, 'invalid-group.json'), JSON.stringify(invalid));
|
||||||
|
|
||||||
|
const invalidMode = structuredClone(source);
|
||||||
|
invalidMode.section[0].dashboard_filter_mode = 'unknown';
|
||||||
|
fs.writeFileSync(path.join(outputDir, 'invalid-mode.json'), JSON.stringify(invalidMode));
|
||||||
|
JS
|
||||||
|
|
||||||
|
for mode in disabled include exclude mixed; do
|
||||||
|
validate_fixture "$WORK_DIR/$mode.json" >/dev/null || fail "$mode dashboard filter validation"
|
||||||
|
generate_config "$WORK_DIR/$mode.json" "$WORK_DIR/$mode-config.json"
|
||||||
|
done
|
||||||
|
|
||||||
|
if validate_fixture "$WORK_DIR/invalid-group.json" >/dev/null 2>&1; then
|
||||||
|
fail "unknown dashboard group should be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if validate_fixture "$WORK_DIR/invalid-mode.json" >/dev/null 2>&1; then
|
||||||
|
fail "unknown dashboard filter mode should be rejected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ucode -e '
|
||||||
|
let fs = require("fs");
|
||||||
|
function fail(message) { die(message + "\n"); }
|
||||||
|
function outbound_by_tag(config, tag) {
|
||||||
|
for (let outbound in config.outbounds || [])
|
||||||
|
if (outbound && outbound.tag == tag)
|
||||||
|
return outbound;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function assert_array(value, expected, label) {
|
||||||
|
value = value || [];
|
||||||
|
if (length(value) != length(expected))
|
||||||
|
fail(label + " length mismatch: " + sprintf("%J", value));
|
||||||
|
for (let i = 0; i < length(expected); i++)
|
||||||
|
if (value[i] != expected[i])
|
||||||
|
fail(label + " mismatch: " + sprintf("%J", value));
|
||||||
|
}
|
||||||
|
let disabled = outbound_by_tag(json(fs.readfile(ARGV[0])), "proxy-out");
|
||||||
|
let include = outbound_by_tag(json(fs.readfile(ARGV[1])), "proxy-out");
|
||||||
|
let exclude = outbound_by_tag(json(fs.readfile(ARGV[2])), "proxy-out");
|
||||||
|
let mixed = outbound_by_tag(json(fs.readfile(ARGV[3])), "proxy-out");
|
||||||
|
assert_array(disabled.outbounds, [ "Alpha", "Beta", "Gamma", "Delta", "Echo", "proxy-urltest-ut_main-out", "proxy-priority-pg_shared-out", "proxy-priority-pg_main-out" ], "all dashboard servers");
|
||||||
|
assert_array(include.outbounds, [ "Alpha", "Beta", "Gamma", "proxy-urltest-ut_main-out", "proxy-priority-pg_shared-out", "proxy-priority-pg_main-out" ], "same-name URLTest and Priority groups");
|
||||||
|
assert_array(exclude.outbounds, [ "Alpha", "Delta", "Echo", "proxy-urltest-ut_main-out", "proxy-priority-pg_shared-out", "proxy-priority-pg_main-out" ], "excluded Priority group");
|
||||||
|
assert_array(mixed.outbounds, [ "Alpha", "Echo", "proxy-urltest-ut_main-out", "proxy-priority-pg_shared-out", "proxy-priority-pg_main-out" ], "mixed group and proxy filters");
|
||||||
|
if (mixed.default != "proxy-urltest-ut_main-out")
|
||||||
|
fail("section selector should still default to the first URLTest group");
|
||||||
|
' "$WORK_DIR/disabled-config.json" "$WORK_DIR/include-config.json" \
|
||||||
|
"$WORK_DIR/exclude-config.json" "$WORK_DIR/mixed-config.json" ||
|
||||||
|
fail "dashboard server filter regression"
|
||||||
|
|
||||||
|
printf 'dashboard server filter checks passed\n'
|
||||||
@@ -52,14 +52,16 @@ const ruleSetOption = requiredIndex("const ruleSetOption = section.taboption(");
|
|||||||
const dnsRuleSetOption = requiredIndex("const dnsRuleSetOption = section.taboption(", ruleSetOption);
|
const dnsRuleSetOption = requiredIndex("const dnsRuleSetOption = section.taboption(", ruleSetOption);
|
||||||
const domainLists = requiredIndex("const domainIpListsOption", dnsRuleSetOption);
|
const domainLists = requiredIndex("const domainIpListsOption", dnsRuleSetOption);
|
||||||
if (!source.slice(ruleSetOption, dnsRuleSetOption).includes("SettingsDynamicList") ||
|
if (!source.slice(ruleSetOption, dnsRuleSetOption).includes("SettingsDynamicList") ||
|
||||||
!source.slice(ruleSetOption, dnsRuleSetOption).includes("dependsOnRoutingAction(ruleSetOption)")) {
|
!source.slice(ruleSetOption, dnsRuleSetOption).includes("dependsOnRoutingAction(ruleSetOption)") ||
|
||||||
fail("routing rule sets must keep their settings DynamicList");
|
!source.slice(ruleSetOption, dnsRuleSetOption).includes("ruleSetOption.retain = true")) {
|
||||||
|
fail("routing rule sets must keep their retained settings DynamicList");
|
||||||
}
|
}
|
||||||
if (!source.slice(dnsRuleSetOption, domainLists).includes("form.DynamicList") ||
|
if (!source.slice(dnsRuleSetOption, domainLists).includes("form.DynamicList") ||
|
||||||
!source.slice(dnsRuleSetOption, domainLists).includes('"_dns_rule_set"') ||
|
!source.slice(dnsRuleSetOption, domainLists).includes('"_dns_rule_set"') ||
|
||||||
!source.slice(dnsRuleSetOption, domainLists).includes('dnsRuleSetOption.depends("action", "dns")') ||
|
!source.slice(dnsRuleSetOption, domainLists).includes('dnsRuleSetOption.depends("action", "dns")') ||
|
||||||
|
!source.slice(dnsRuleSetOption, domainLists).includes("dnsRuleSetOption.retain = true") ||
|
||||||
!source.slice(dnsRuleSetOption, domainLists).includes("writeDnsRulesetReferences")) {
|
!source.slice(dnsRuleSetOption, domainLists).includes("writeDnsRulesetReferences")) {
|
||||||
fail("DNS rule sets must use a dedicated classic DynamicList");
|
fail("DNS rule sets must use a retained classic DynamicList");
|
||||||
}
|
}
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ wrapper_styles="$(sed -n '/^\.fkp-button-add-dynlist > \.add-item {$/,/^}$/p' "$
|
|||||||
button_styles="$(sed -n '/^\.fkp-button-add-dynlist > \.add-item > \.cbi-button-add {$/,/^}$/p' "$SECTION_JS")"
|
button_styles="$(sed -n '/^\.fkp-button-add-dynlist > \.add-item > \.cbi-button-add {$/,/^}$/p' "$SECTION_JS")"
|
||||||
urltest_options="$(sed -n '/^function addUrlTestItemOptions(/,/^function priorityLevelSettingsForValidation(/p' "$SECTION_JS")"
|
urltest_options="$(sed -n '/^function addUrlTestItemOptions(/,/^function priorityLevelSettingsForValidation(/p' "$SECTION_JS")"
|
||||||
priority_options="$(sed -n '/^function addPriorityLevelItemOptions(/,/^function addPriorityGroupItemOptions(/p' "$SECTION_JS")"
|
priority_options="$(sed -n '/^function addPriorityLevelItemOptions(/,/^function addPriorityGroupItemOptions(/p' "$SECTION_JS")"
|
||||||
|
dashboard_options="$(sed -n '/^function addDashboardServerFilterOptions(/,/^function settingValueEquals(/p' "$SECTION_JS")"
|
||||||
|
create_section="$(sed -n '/^function createSectionContent(/,/^function loadSectionTableOptions(/p' "$SECTION_JS")"
|
||||||
|
live_choices="$(sed -n '/^function configureLiveDynamicListChoices(/,/^function countryChoices(/p' "$SECTION_JS")"
|
||||||
|
|
||||||
grep -Fq 'display: flex;' <<<"$wrapper_styles" ||
|
grep -Fq 'display: flex;' <<<"$wrapper_styles" ||
|
||||||
fail "button-only DynamicList add rows must use a content-sized flex wrapper"
|
fail "button-only DynamicList add rows must use a content-sized flex wrapper"
|
||||||
@@ -36,10 +39,14 @@ grep -Fq 'if (key === "include_regex") {' <<<"$urltest_options" ||
|
|||||||
fail "URLTest include proxy parameters must follow the include regex option"
|
fail "URLTest include proxy parameters must follow the include regex option"
|
||||||
grep -Fq 'else if (key === "exclude_regex") {' <<<"$urltest_options" ||
|
grep -Fq 'else if (key === "exclude_regex") {' <<<"$urltest_options" ||
|
||||||
fail "URLTest exclude proxy parameters must follow the exclude regex option"
|
fail "URLTest exclude proxy parameters must follow the exclude regex option"
|
||||||
|
grep -Fq 'configureLiveDynamicListChoices(list, (itemId, values) =>' <<<"$urltest_options" ||
|
||||||
|
fail "URLTest server filters must stay comboboxes when no choices exist"
|
||||||
grep -Fq 'if (key === "regex") {' <<<"$priority_options" ||
|
grep -Fq 'if (key === "regex") {' <<<"$priority_options" ||
|
||||||
fail "Priority include proxy parameters must follow the include regex option"
|
fail "Priority include proxy parameters must follow the include regex option"
|
||||||
grep -Fq 'else if (key === "exclude_regex") {' <<<"$priority_options" ||
|
grep -Fq 'else if (key === "exclude_regex") {' <<<"$priority_options" ||
|
||||||
fail "Priority exclude proxy parameters must follow the exclude regex option"
|
fail "Priority exclude proxy parameters must follow the exclude regex option"
|
||||||
|
grep -Fq 'configureLiveDynamicListChoices(list, (itemId, values) =>' <<<"$priority_options" ||
|
||||||
|
fail "Priority server filters must stay comboboxes when no choices exist"
|
||||||
grep -Fq '["direct", "Direct"]' "$SECTION_JS" ||
|
grep -Fq '["direct", "Direct"]' "$SECTION_JS" ||
|
||||||
fail "proxy protocol choices must keep Direct untranslated"
|
fail "proxy protocol choices must keep Direct untranslated"
|
||||||
grep -Fq '["none", "None"]' "$SECTION_JS" ||
|
grep -Fq '["none", "None"]' "$SECTION_JS" ||
|
||||||
@@ -49,5 +56,76 @@ grep -Fq '_("Security")' "$SECTION_JS" ||
|
|||||||
if grep -Fq 'Connection security' "$SECTION_JS"; then
|
if grep -Fq 'Connection security' "$SECTION_JS"; then
|
||||||
fail "proxy parameter filter copy must not mention connection security"
|
fail "proxy parameter filter copy must not mention connection security"
|
||||||
fi
|
fi
|
||||||
|
if grep -Fq 'hide_added_outbounds' "$SECTION_JS"; then
|
||||||
|
fail "per-group dashboard hiding must be removed from LuCI"
|
||||||
|
fi
|
||||||
|
grep -Fq '"dashboard_filter_mode"' <<<"$dashboard_options" ||
|
||||||
|
fail "section settings must expose the dashboard server filter"
|
||||||
|
grep -Fq '_("Servers on dashboard")' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard server filter must use the requested section-level label"
|
||||||
|
grep -Fq '_("Filter the servers that will be displayed on the dashboard.")' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard server filter must use the requested description"
|
||||||
|
grep -Fq 'option.modalonly = true;' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard server filter options must stay inside the section modal"
|
||||||
|
grep -Fq '"dashboard_include_groups"' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard include filters must support URLTest and Priority groups"
|
||||||
|
grep -Fq '"dashboard_exclude_groups"' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard exclude filters must support URLTest and Priority groups"
|
||||||
|
include_group_line="$(grep -n '"dashboard_include_groups"' <<<"$dashboard_options" | head -n1 | cut -d: -f1)"
|
||||||
|
include_proxy_line="$(grep -n 'includeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
|
||||||
|
[[ "$include_group_line" -lt "$include_proxy_line" ]] ||
|
||||||
|
fail "dashboard include group selector must precede the proxy-parameter toggle"
|
||||||
|
exclude_group_line="$(grep -n '"dashboard_exclude_groups"' <<<"$dashboard_options" | head -n1 | cut -d: -f1)"
|
||||||
|
exclude_proxy_line="$(grep -n 'excludeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
|
||||||
|
[[ "$exclude_group_line" -lt "$exclude_proxy_line" ]] ||
|
||||||
|
fail "dashboard exclude group selector must precede the proxy-parameter toggle"
|
||||||
|
grep -Fq 'const liveValues = currentLiveDynamicListValues(section_id, typeName);' "$SECTION_JS" ||
|
||||||
|
fail "dashboard group choices must read live uncommitted DynamicList values"
|
||||||
|
grep -Fq 'o.onListChange = refreshDashboardFilterChoiceWidgets;' "$SECTION_JS" ||
|
||||||
|
fail "new servers and groups must refresh dashboard selectors immediately"
|
||||||
|
grep -Fq 'form.DynamicList,' <<<"$(sed -n '/^function addDashboardGroupFilterOption(/,/^function addDashboardServerFilterOptions(/p' "$SECTION_JS")" ||
|
||||||
|
fail "dashboard group filters must use the standard DynamicList"
|
||||||
|
if grep -Fq 'SelectOnlyUIDynamicList' "$SECTION_JS"; then
|
||||||
|
fail "dashboard group filters must not replace the standard DynamicList widget"
|
||||||
|
fi
|
||||||
|
grep -Fq 'configureLiveDynamicListChoices(list, currentOutboundNameChoices);' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard server selectors must refresh live uncommitted outbound choices"
|
||||||
|
grep -Fq 'configureLiveDynamicListChoices(list, currentSectionGroupChoices);' "$SECTION_JS" ||
|
||||||
|
fail "dashboard group selectors must refresh live uncommitted group choices"
|
||||||
|
grep -Fq 'const widget = new ui.DynamicList(values, labels, {' <<<"$live_choices" ||
|
||||||
|
fail "dashboard selectors must always render the stock DynamicList combobox"
|
||||||
|
grep -Fq 'validate: L.bind(this.validate, this, section_id),' <<<"$live_choices" ||
|
||||||
|
fail "dashboard selectors must use the LuCI 24.10-compatible validator binding"
|
||||||
|
if grep -Fq 'this.getValidator(section_id)' <<<"$live_choices"; then
|
||||||
|
fail "dashboard selectors must not require the newer LuCI getValidator API"
|
||||||
|
fi
|
||||||
|
grep -Fq 'const node = widget.render();' <<<"$live_choices" ||
|
||||||
|
fail "dashboard selectors must refresh their rendered DynamicList instance directly"
|
||||||
|
if grep -Fq 'form.DynamicList.prototype.renderWidget' <<<"$live_choices"; then
|
||||||
|
fail "empty dashboard choices must not fall back to a plain DynamicList input"
|
||||||
|
fi
|
||||||
|
grep -Fq 'dashboardFilterChoiceRefreshers.get(section_id).add(refreshChoices);' <<<"$live_choices" ||
|
||||||
|
fail "dashboard selectors must register the rendered modal widget for source changes"
|
||||||
|
grep -Fq 'node.addEventListener("mousedown", refreshBeforeOpening, true);' <<<"$live_choices" ||
|
||||||
|
fail "dashboard selectors must refresh choices before opening"
|
||||||
|
grep -Fq 'if (currentSignature === choiceSignature)' "$SECTION_JS" ||
|
||||||
|
fail "dashboard selectors must not rebuild unchanged choices while selecting an item"
|
||||||
|
grep -Fq 'loadOutboundNameChoices(section_id).then(() => {' <<<"$dashboard_options" ||
|
||||||
|
fail "dashboard server choices must refresh after metadata loads"
|
||||||
|
if grep -Fq 'return loadOutboundNameChoices(section_id)' <<<"$dashboard_options"; then
|
||||||
|
fail "dashboard metadata loading must not block the section modal"
|
||||||
|
fi
|
||||||
|
grep -Fq 'result.push({ value: name, label: name });' "$SECTION_JS" ||
|
||||||
|
fail "dashboard group selector values must use display names"
|
||||||
|
if grep -Fq 'label: `${typeLabel}: ${name}`' "$SECTION_JS"; then
|
||||||
|
fail "dashboard group labels must not include URLTest/Priority prefixes"
|
||||||
|
fi
|
||||||
|
ports_line="$(grep -n 'dependsOnRoutingAction(portsOption);' <<<"$create_section" | cut -d: -f1)"
|
||||||
|
dashboard_line="$(grep -n 'addDashboardServerFilterOptions(section);' <<<"$create_section" | cut -d: -f1)"
|
||||||
|
[[ "$dashboard_line" -gt "$ports_line" ]] ||
|
||||||
|
fail "dashboard server filter must be the last section option block"
|
||||||
|
if grep -Fq '__forkop_no_group__' "$SECTION_JS"; then
|
||||||
|
fail "dashboard group selectors must not duplicate the placeholder with a fake choice"
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'LuCI DynamicList layout checks passed\n'
|
printf 'LuCI DynamicList layout checks passed\n'
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ cat >"$WORK_DIR/fixture.json" <<'JSON'
|
|||||||
"pick_fastest": "1",
|
"pick_fastest": "1",
|
||||||
"switch_to_faster_same_priority": "1",
|
"switch_to_faster_same_priority": "1",
|
||||||
"fastest_check_interval": "3m",
|
"fastest_check_interval": "3m",
|
||||||
"pin_dashboard": "1",
|
"pin_dashboard": "1"
|
||||||
"hide_added_outbounds": "1"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
".name": "pg_backup",
|
".name": "pg_backup",
|
||||||
@@ -189,8 +188,8 @@ if (main.default != "proxy-2-out")
|
|||||||
fail("main priority selector should default to the first upper-level outbound");
|
fail("main priority selector should default to the first upper-level outbound");
|
||||||
if (!selector || selector.default != "proxy-priority-pg_main-out")
|
if (!selector || selector.default != "proxy-priority-pg_main-out")
|
||||||
fail("section selector should default to the first priority group when URLTest is absent");
|
fail("section selector should default to the first priority group when URLTest is absent");
|
||||||
if (contains(selector.outbounds, "proxy-2-out") || contains(selector.outbounds, "proxy-3-out"))
|
if (!contains(selector.outbounds, "proxy-2-out") || !contains(selector.outbounds, "proxy-3-out"))
|
||||||
fail("priority-added outbounds should be hidden from selector when hide_added_outbounds=1");
|
fail("section selector should keep individual priority members by default");
|
||||||
if (selector.outbounds[length(selector.outbounds || []) - 2] != "proxy-priority-pg_main-out" ||
|
if (selector.outbounds[length(selector.outbounds || []) - 2] != "proxy-priority-pg_main-out" ||
|
||||||
selector.outbounds[length(selector.outbounds || []) - 1] != "proxy-priority-pg_backup-out")
|
selector.outbounds[length(selector.outbounds || []) - 1] != "proxy-priority-pg_backup-out")
|
||||||
fail("section selector should include priority groups in order");
|
fail("section selector should include priority groups in order");
|
||||||
@@ -205,7 +204,7 @@ if (cached.check_timeout != "2s" || cached.fastest_check_interval != "3m")
|
|||||||
fail("priority group timing metadata was not cached");
|
fail("priority group timing metadata was not cached");
|
||||||
if (cached.pick_fastest !== true || cached.switch_to_faster_same_priority !== true)
|
if (cached.pick_fastest !== true || cached.switch_to_faster_same_priority !== true)
|
||||||
fail("priority boolean metadata was not cached");
|
fail("priority boolean metadata was not cached");
|
||||||
if (cached.interrupt_exist_connections !== true || cached.pin_dashboard !== true || cached.hide_added_outbounds !== true)
|
if (cached.interrupt_exist_connections !== true || cached.pin_dashboard !== true)
|
||||||
fail("priority dashboard/default metadata was not cached");
|
fail("priority dashboard/default metadata was not cached");
|
||||||
assert_array(cached.outbounds, [ "proxy-2-out", "proxy-3-out" ], "cached priority outbounds");
|
assert_array(cached.outbounds, [ "proxy-2-out", "proxy-3-out" ], "cached priority outbounds");
|
||||||
if (length(cached.levels || []) != 2 || cached.levels[0].id != "pl_upper" || cached.levels[1].id != "pl_lower")
|
if (length(cached.levels || []) != 2 || cached.levels[0].id != "pl_upper" || cached.levels[1].id != "pl_lower")
|
||||||
|
|||||||
@@ -408,6 +408,24 @@ cat >"$WORK_DIR/sing-box-signature.json" <<'JSON'
|
|||||||
"enabled": "1",
|
"enabled": "1",
|
||||||
"action": "proxy",
|
"action": "proxy",
|
||||||
"selector_proxy_links": [ "vless://one", "vless://two" ],
|
"selector_proxy_links": [ "vless://one", "vless://two" ],
|
||||||
|
"dashboard_filter_mode": "mixed",
|
||||||
|
"dashboard_detect_server_country": "country_is",
|
||||||
|
"dashboard_include_countries": [ "NL" ],
|
||||||
|
"dashboard_include_outbounds": [ "node-a" ],
|
||||||
|
"dashboard_include_regex": [ "^A" ],
|
||||||
|
"dashboard_include_proxy_parameters": "1",
|
||||||
|
"dashboard_include_protocols": [ "vless" ],
|
||||||
|
"dashboard_include_transports": [ "ws" ],
|
||||||
|
"dashboard_include_securities": [ "reality" ],
|
||||||
|
"dashboard_include_groups": [ "urltest" ],
|
||||||
|
"dashboard_exclude_countries": [ "RU" ],
|
||||||
|
"dashboard_exclude_outbounds": [ "node-b" ],
|
||||||
|
"dashboard_exclude_regex": [ "backup" ],
|
||||||
|
"dashboard_exclude_proxy_parameters": "1",
|
||||||
|
"dashboard_exclude_protocols": [ "http" ],
|
||||||
|
"dashboard_exclude_transports": [ "tcp" ],
|
||||||
|
"dashboard_exclude_securities": [ "none" ],
|
||||||
|
"dashboard_exclude_groups": [ "urltest" ],
|
||||||
"subscription_urls": [ "https://example.com/sub.txt" ],
|
"subscription_urls": [ "https://example.com/sub.txt" ],
|
||||||
"subscription_url_settings": "{\"https://example.com/sub.txt\":{\"prefix_nodes\":\"1\",\"node_prefix\":\"Example\"}}",
|
"subscription_url_settings": "{\"https://example.com/sub.txt\":{\"prefix_nodes\":\"1\",\"node_prefix\":\"Example\"}}",
|
||||||
"urltest_enabled": "1",
|
"urltest_enabled": "1",
|
||||||
@@ -559,9 +577,11 @@ proxy
|
|||||||
[rule.proxy1.legacy_outbound_json]
|
[rule.proxy1.legacy_outbound_json]
|
||||||
|
|
||||||
[rule.proxy1.urltests]
|
[rule.proxy1.urltests]
|
||||||
[ { "id": "urltest", "display_name": "Fastest", "check_interval": "3m", "tolerance": "75", "testing_url": "https://www.gstatic.com/generate_204", "idle_timeout": "", "interrupt_exist_connections": "1", "pin_dashboard": "1", "hide_added_outbounds": "0", "filter_mode": "exclude", "detect_server_country": "1", "include_countries": [ ], "include_outbounds": [ "node-a" ], "include_regex": [ ], "exclude_countries": [ "RU" ], "exclude_outbounds": [ ], "exclude_regex": [ ] } ]
|
[ { "id": "urltest", "display_name": "Fastest", "check_interval": "3m", "tolerance": "75", "testing_url": "https://www.gstatic.com/generate_204", "idle_timeout": "", "interrupt_exist_connections": "1", "pin_dashboard": "1", "filter_mode": "exclude", "detect_server_country": "1", "include_countries": [ ], "include_outbounds": [ "node-a" ], "include_regex": [ ], "exclude_countries": [ "RU" ], "exclude_outbounds": [ ], "exclude_regex": [ ] } ]
|
||||||
[rule.proxy1.priority_groups]
|
[rule.proxy1.priority_groups]
|
||||||
[ ]
|
[ ]
|
||||||
|
[rule.proxy1.dashboard_filter]
|
||||||
|
{ "filter_mode": "mixed", "detect_server_country": "country_is", "include_countries": [ "NL" ], "include_outbounds": [ "node-a" ], "include_regex": [ "^A" ], "include_proxy_parameters": "1", "include_protocols": [ "vless" ], "include_transports": [ "ws" ], "include_securities": [ "reality" ], "include_groups": [ "urltest" ], "exclude_countries": [ "RU" ], "exclude_outbounds": [ "node-b" ], "exclude_regex": [ "backup" ], "exclude_proxy_parameters": "1", "exclude_protocols": [ "http" ], "exclude_transports": [ "tcp" ], "exclude_securities": [ "none" ], "exclude_groups": [ "urltest" ] }
|
||||||
[rule.proxy1.urltest_enabled]
|
[rule.proxy1.urltest_enabled]
|
||||||
1
|
1
|
||||||
[rule.proxy1.detect_server_country]
|
[rule.proxy1.detect_server_country]
|
||||||
@@ -646,6 +666,8 @@ outbound
|
|||||||
[ ]
|
[ ]
|
||||||
[rule.out1.priority_groups]
|
[rule.out1.priority_groups]
|
||||||
[ ]
|
[ ]
|
||||||
|
[rule.out1.dashboard_filter]
|
||||||
|
{ "filter_mode": "disabled", "detect_server_country": "flag_emoji", "include_countries": [ ], "include_outbounds": [ ], "include_regex": [ ], "include_proxy_parameters": "0", "include_protocols": [ ], "include_transports": [ ], "include_securities": [ ], "include_groups": [ ], "exclude_countries": [ ], "exclude_outbounds": [ ], "exclude_regex": [ ], "exclude_proxy_parameters": "0", "exclude_protocols": [ ], "exclude_transports": [ ], "exclude_securities": [ ], "exclude_groups": [ ] }
|
||||||
[rule.out1.urltest_enabled]
|
[rule.out1.urltest_enabled]
|
||||||
0
|
0
|
||||||
[rule.out1.detect_server_country]
|
[rule.out1.detect_server_country]
|
||||||
@@ -752,6 +774,8 @@ wg0
|
|||||||
[ ]
|
[ ]
|
||||||
[rule.vpn1.priority_groups]
|
[rule.vpn1.priority_groups]
|
||||||
[ ]
|
[ ]
|
||||||
|
[rule.vpn1.dashboard_filter]
|
||||||
|
{ "filter_mode": "disabled", "detect_server_country": "flag_emoji", "include_countries": [ ], "include_outbounds": [ ], "include_regex": [ ], "include_proxy_parameters": "0", "include_protocols": [ ], "include_transports": [ ], "include_securities": [ ], "include_groups": [ ], "exclude_countries": [ ], "exclude_outbounds": [ ], "exclude_regex": [ ], "exclude_proxy_parameters": "0", "exclude_protocols": [ ], "exclude_transports": [ ], "exclude_securities": [ ], "exclude_groups": [ ] }
|
||||||
[rule.vpn1.urltest_enabled]
|
[rule.vpn1.urltest_enabled]
|
||||||
0
|
0
|
||||||
[rule.vpn1.detect_server_country]
|
[rule.vpn1.detect_server_country]
|
||||||
|
|||||||
@@ -185,76 +185,4 @@ if (cache.urltestGroups["proxy-urltest-cfg010002-out"].displayName != "Stable UR
|
|||||||
die("stable URLTest display name was not cached\n");
|
die("stable URLTest display name was not cached\n");
|
||||||
' "$multi_output" "$multi_output.section-cache/proxy.json" || fail "multi URLTest settings regression"
|
' "$multi_output" "$multi_output.section-cache/proxy.json" || fail "multi URLTest settings regression"
|
||||||
|
|
||||||
cat >"$WORK_DIR/hide-added-fixture.json" <<'JSON'
|
|
||||||
{
|
|
||||||
"settings": {
|
|
||||||
"interface": "lan",
|
|
||||||
"routing_mode": "proxy"
|
|
||||||
},
|
|
||||||
"section": [
|
|
||||||
{
|
|
||||||
".name": "proxy",
|
|
||||||
".type": "section",
|
|
||||||
"enabled": "1",
|
|
||||||
"action": "proxy",
|
|
||||||
"selector_proxy_links": [
|
|
||||||
"vless://00000000-0000-4000-8000-000000000001@example.com:443?encryption=none&security=tls&sni=example.com#first",
|
|
||||||
"vless://00000000-0000-4000-8000-000000000002@example.org:443?encryption=none&security=tls&sni=example.org#second"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"urltest": [
|
|
||||||
{
|
|
||||||
".name": "cfg020001",
|
|
||||||
".type": "urltest",
|
|
||||||
"section": "proxy",
|
|
||||||
"name": "Fast URLTest",
|
|
||||||
"check_interval": "30s",
|
|
||||||
"tolerance": "50",
|
|
||||||
"testing_url": "https://fast.example/204",
|
|
||||||
"filter_mode": "include",
|
|
||||||
"include_regex": [ "first" ],
|
|
||||||
"hide_added_outbounds": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
JSON
|
|
||||||
|
|
||||||
hide_added_output="$WORK_DIR/hide-added-config.json"
|
|
||||||
mkdir -p "$hide_added_output.section-cache"
|
|
||||||
ucode -L "$FORKOP_LIB" "$FORKOP_LIB/singbox/generator.uc" generate-config-fixture \
|
|
||||||
"$WORK_DIR/hide-added-fixture.json" "$hide_added_output" "127.0.0.1"
|
|
||||||
|
|
||||||
ucode -e '
|
|
||||||
let fs = require("fs");
|
|
||||||
let cfg = json(fs.readfile(ARGV[0]));
|
|
||||||
let urltest = null;
|
|
||||||
let selector = null;
|
|
||||||
|
|
||||||
function contains(values, needle) {
|
|
||||||
for (let value in values || [])
|
|
||||||
if (value == needle)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let outbound in cfg.outbounds || []) {
|
|
||||||
if (outbound && outbound.tag == "proxy-urltest-cfg020001-out")
|
|
||||||
urltest = outbound;
|
|
||||||
if (outbound && outbound.tag == "proxy-out")
|
|
||||||
selector = outbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!urltest || !contains(urltest.outbounds, "proxy-1-out") || contains(urltest.outbounds, "proxy-2-out"))
|
|
||||||
die("URLTest should contain only the filtered first outbound\n");
|
|
||||||
if (!selector)
|
|
||||||
die("selector was not generated\n");
|
|
||||||
if (contains(selector.outbounds, "proxy-1-out"))
|
|
||||||
die("URLTest-added outbound should be hidden from selector\n");
|
|
||||||
if (!contains(selector.outbounds, "proxy-2-out"))
|
|
||||||
die("non-URLTest outbound should remain in selector\n");
|
|
||||||
if (!contains(selector.outbounds, "proxy-urltest-cfg020001-out"))
|
|
||||||
die("URLTest outbound should remain in selector\n");
|
|
||||||
' "$hide_added_output" || fail "URLTest hide added selector regression"
|
|
||||||
|
|
||||||
printf 'URLTest interrupt checks passed\n'
|
printf 'URLTest interrupt checks passed\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user