Improve URLTest details and Xray tag handling
This commit is contained in:
+184
-197
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-02 17:43+0300\n"
|
||||
"PO-Revision-Date: 2026-07-02 17:43+0300\n"
|
||||
"POT-Creation-Date: 2026-07-02 23:54+0300\n"
|
||||
"PO-Revision-Date: 2026-07-02 23:54+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -281,9 +281,6 @@ msgstr "Копировать"
|
||||
msgid "Copy proxy link"
|
||||
msgstr "Скопировать ссылку прокси"
|
||||
|
||||
msgid "Copy URL"
|
||||
msgstr "Скопировать URL"
|
||||
|
||||
msgid "Could not compare FakeIP and control public IPs"
|
||||
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
|
||||
|
||||
@@ -623,8 +620,8 @@ msgstr "Задержка при мониторинге интерфейсов"
|
||||
msgid "Interface monitoring for Bad WAN"
|
||||
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||
|
||||
msgid "Interrupt existing connections"
|
||||
msgstr "Прерывать существующие соединения"
|
||||
msgid "Interrupt connections"
|
||||
msgstr "Прерывать соединения"
|
||||
|
||||
msgid "Interval"
|
||||
msgstr "Интервал"
|
||||
@@ -911,9 +908,6 @@ msgstr "Мониторинг"
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr "Должно быть числом от 50 до 1000"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Н/Д"
|
||||
|
||||
msgid "Network Interface"
|
||||
msgstr "Сетевой интерфейс"
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ function isUrlTestProxyEntry(entry?: ClashProxyEntry) {
|
||||
return entry?.value?.type?.toLowerCase() === 'urltest';
|
||||
}
|
||||
|
||||
function getLatencySortValue(outbound: Podkop.Outbound) {
|
||||
function getLatencySortValue(outbound: { latency: number }) {
|
||||
const latency = Number(outbound.latency);
|
||||
|
||||
return Number.isFinite(latency) && latency > 0
|
||||
@@ -248,6 +248,27 @@ function sortOutboundsForDashboard(
|
||||
.map((item) => item.outbound);
|
||||
}
|
||||
|
||||
function sortUrlTestMembers(outbounds: Podkop.UrlTestMember[]) {
|
||||
return outbounds
|
||||
.map((outbound, index) => ({ outbound, index }))
|
||||
.sort((left, right) => {
|
||||
if (left.outbound.selected !== right.outbound.selected) {
|
||||
return left.outbound.selected ? -1 : 1;
|
||||
}
|
||||
|
||||
const latencyDiff =
|
||||
getLatencySortValue(left.outbound) -
|
||||
getLatencySortValue(right.outbound);
|
||||
|
||||
if (latencyDiff !== 0) {
|
||||
return latencyDiff;
|
||||
}
|
||||
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map((item) => item.outbound);
|
||||
}
|
||||
|
||||
function isSafeSectionName(sectionName: string) {
|
||||
return /^[A-Za-z0-9_-]+$/.test(sectionName);
|
||||
}
|
||||
@@ -338,32 +359,34 @@ function buildUrlTestInfo({
|
||||
: entry.value.all || [],
|
||||
);
|
||||
const selectedCode = entry.value.now || '';
|
||||
const outbounds = childCodes.flatMap((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || '';
|
||||
const canCopyLink =
|
||||
isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
const outbounds = sortUrlTestMembers(
|
||||
childCodes.flatMap((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || '';
|
||||
const canCopyLink =
|
||||
isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
|
||||
return [
|
||||
{
|
||||
code: childCode,
|
||||
displayName: getOutboundDisplayName(
|
||||
childCode,
|
||||
childEntry,
|
||||
return [
|
||||
{
|
||||
code: childCode,
|
||||
displayName: getOutboundDisplayName(
|
||||
childCode,
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata,
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
outboundMetadata,
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || '',
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
canCopyLink,
|
||||
country: showDetectedCountries
|
||||
? outboundMetadata?.countries?.[childCode]
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
});
|
||||
canCopyLink,
|
||||
country: showDetectedCountries
|
||||
? outboundMetadata?.countries?.[childCode]
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
const selectedName =
|
||||
outbounds.find((outbound) => outbound.code === selectedCode)?.displayName ||
|
||||
selectedCode;
|
||||
|
||||
@@ -125,6 +125,20 @@ describe('getDashboardSections', () => {
|
||||
|
||||
it('hydrates URLTest details from the section cache and Clash API', async () => {
|
||||
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
||||
mocks.getClashApiProxies.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
proxies: {
|
||||
...clashProxies,
|
||||
'main-urltest-out': proxy('URLTest', {
|
||||
name: 'main-urltest-out',
|
||||
now: 'main-3-out',
|
||||
history: [{ time: '2026-05-27T00:00:00Z', delay: 10 }],
|
||||
all: ['main-1-out', 'main-3-out'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.fsRead.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
outboundMetadata: {
|
||||
@@ -137,7 +151,7 @@ describe('getDashboardSections', () => {
|
||||
urltestGroups: {
|
||||
'main-urltest-out': {
|
||||
displayName: 'Fastest',
|
||||
outbounds: ['main-1-out', 'main-3-out'],
|
||||
outbounds: ['main-1-out', 'main-3-out', 'main-2-out'],
|
||||
url: 'https://probe.example/204',
|
||||
interval: '3m',
|
||||
tolerance: 50,
|
||||
@@ -161,15 +175,24 @@ describe('getDashboardSections', () => {
|
||||
interval: '3m',
|
||||
tolerance: 50,
|
||||
interruptExistConnections: true,
|
||||
selectedCode: 'main-3-out',
|
||||
selectedName: 'Third cached',
|
||||
});
|
||||
expect(urltest?.urlTestInfo?.outbounds.map((item) => item.code)).toEqual([
|
||||
'main-1-out',
|
||||
'main-3-out',
|
||||
'main-1-out',
|
||||
'main-2-out',
|
||||
]);
|
||||
expect(urltest?.urlTestInfo?.outbounds[0]).toMatchObject({
|
||||
displayName: 'Third cached',
|
||||
latency: 300,
|
||||
type: 'VLESS',
|
||||
selected: true,
|
||||
});
|
||||
expect(urltest?.urlTestInfo?.outbounds[1]).toMatchObject({
|
||||
displayName: 'one',
|
||||
latency: 100,
|
||||
type: 'VLESS',
|
||||
selected: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -613,7 +613,68 @@ function formatUrlTestModalValue(value: unknown) {
|
||||
}
|
||||
|
||||
const text = `${value ?? ''}`.trim();
|
||||
return text || _('N/A');
|
||||
return text || _('No');
|
||||
}
|
||||
|
||||
function getUrlTestLatencyClass(latency: number) {
|
||||
if (!latency) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
|
||||
}
|
||||
|
||||
if (latency < 800) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
|
||||
}
|
||||
|
||||
if (latency < 1500) {
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
|
||||
}
|
||||
|
||||
return 'pdk_dashboard-page__outbound-grid__item__latency--red';
|
||||
}
|
||||
|
||||
function formatUrlTestLatency(latency: number) {
|
||||
return latency ? `${latency}ms` : 'N/A';
|
||||
}
|
||||
|
||||
function renderUrlTestSelectedValue(info: Podkop.UrlTestInfo) {
|
||||
const selectedMember = info.outbounds.find((member) => member.selected);
|
||||
const selectedName =
|
||||
selectedMember?.displayName || info.selectedName || info.selectedCode || '';
|
||||
const name = formatUrlTestModalValue(selectedName);
|
||||
|
||||
if (name === _('No')) {
|
||||
return E('span', {}, name);
|
||||
}
|
||||
|
||||
return E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__selected-value' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__selected-name' },
|
||||
name,
|
||||
),
|
||||
...(selectedMember?.type
|
||||
? [
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__selected-type' },
|
||||
selectedMember.type,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(selectedMember
|
||||
? [
|
||||
E(
|
||||
'span',
|
||||
{ class: getUrlTestLatencyClass(selectedMember.latency) },
|
||||
formatUrlTestLatency(selectedMember.latency),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function renderUrlTestCopyButton(
|
||||
@@ -624,7 +685,7 @@ function renderUrlTestCopyButton(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class: 'btn pdk_dashboard-page__outbound-grid__item__copy-button',
|
||||
class: 'btn pdk_dashboard-page__urltest-details__copy-button',
|
||||
title,
|
||||
'aria-label': title,
|
||||
click: onClick,
|
||||
@@ -643,33 +704,37 @@ function renderUrlTestInfoModal(
|
||||
return E('div', {}, _('URLTest details are unavailable'));
|
||||
}
|
||||
|
||||
const fields: Array<[string, unknown, string?]> = [
|
||||
[_('Selected'), info.selectedName || info.selectedCode],
|
||||
[_('Testing URL'), info.url, info.url],
|
||||
[_('Interval'), info.interval],
|
||||
[_('Tolerance'), info.tolerance],
|
||||
[_('Idle timeout'), info.idleTimeout],
|
||||
[_('Interrupt existing connections'), info.interruptExistConnections],
|
||||
const fields: Array<{
|
||||
label: string;
|
||||
value?: unknown;
|
||||
children?: Array<HTMLElement | string>;
|
||||
}> = [
|
||||
{
|
||||
label: _('Selected'),
|
||||
children: [renderUrlTestSelectedValue(info)],
|
||||
},
|
||||
{ label: _('Testing URL'), value: info.url },
|
||||
{ label: _('Interval'), value: info.interval },
|
||||
{ label: _('Tolerance'), value: info.tolerance },
|
||||
{ label: _('Idle timeout'), value: info.idleTimeout },
|
||||
{
|
||||
label: _('Interrupt connections'),
|
||||
value: info.interruptExistConnections,
|
||||
},
|
||||
];
|
||||
|
||||
return E('div', { class: 'pdk_dashboard-page__urltest-details' }, [
|
||||
E(
|
||||
'dl',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__params' },
|
||||
fields.map(([label, value, copyValue]) =>
|
||||
fields.map(({ label, value, children }) =>
|
||||
E('div', { class: 'pdk_dashboard-page__urltest-details__param' }, [
|
||||
E('dt', {}, label),
|
||||
E('dd', {}, [
|
||||
E('span', {}, formatUrlTestModalValue(value)),
|
||||
...(copyValue
|
||||
? [
|
||||
renderUrlTestCopyButton(_('Copy URL'), (event) => {
|
||||
event.preventDefault();
|
||||
copyToClipboard(copyValue);
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
E(
|
||||
'dd',
|
||||
{},
|
||||
children || [E('span', {}, formatUrlTestModalValue(value))],
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
@@ -704,7 +769,18 @@ function renderUrlTestInfoModal(
|
||||
},
|
||||
[
|
||||
E('b', {}, member.displayName),
|
||||
member.selected ? E('span', {}, _('Selected')) : '',
|
||||
...(member.type
|
||||
? [
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class:
|
||||
'pdk_dashboard-page__urltest-details__row-type',
|
||||
},
|
||||
member.type,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
),
|
||||
E(
|
||||
@@ -713,29 +789,45 @@ function renderUrlTestInfoModal(
|
||||
class: 'pdk_dashboard-page__urltest-details__row-meta',
|
||||
},
|
||||
[
|
||||
E('span', {}, member.type || _('N/A')),
|
||||
E(
|
||||
'span',
|
||||
{},
|
||||
member.latency ? `${member.latency}ms` : 'N/A',
|
||||
{ class: getUrlTestLatencyClass(member.latency) },
|
||||
formatUrlTestLatency(member.latency),
|
||||
),
|
||||
],
|
||||
),
|
||||
...(member.canCopyLink
|
||||
? [
|
||||
renderUrlTestCopyButton(
|
||||
_('Copy proxy link'),
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
},
|
||||
),
|
||||
]
|
||||
: []),
|
||||
member.canCopyLink
|
||||
? renderUrlTestCopyButton(_('Copy proxy link'), (event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
})
|
||||
: E('span', {
|
||||
class:
|
||||
'pdk_dashboard-page__urltest-details__copy-placeholder',
|
||||
}),
|
||||
],
|
||||
),
|
||||
)
|
||||
: [E('div', {}, _('No outbounds'))],
|
||||
: [
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__urltest-details__empty' },
|
||||
_('No outbounds'),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
E('div', { class: 'pdk_dashboard-page__urltest-details__footer' }, [
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class: 'btn cbi-button cbi-button-neutral',
|
||||
click: () => {
|
||||
ui.hideModal();
|
||||
},
|
||||
},
|
||||
_('Close'),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
@@ -749,7 +841,12 @@ function handleShowUrlTestInfo(
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal(_('URLTest details'), renderUrlTestInfoModal(section, outbound));
|
||||
ui.showModal(
|
||||
`${_('URLTest details')}: ${
|
||||
outbound.urlTestInfo.displayName || outbound.displayName
|
||||
}`,
|
||||
renderUrlTestInfoModal(section, outbound),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleUpdateSubscription(section: Podkop.OutboundGroup) {
|
||||
|
||||
@@ -392,14 +392,17 @@ export const styles = `
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details {
|
||||
min-width: min(720px, 86vw);
|
||||
box-sizing: border-box;
|
||||
width: min(760px, calc(100vw - 56px));
|
||||
max-width: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__params {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, max-content) minmax(0, 1fr);
|
||||
gap: 8px 16px;
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param {
|
||||
@@ -408,6 +411,7 @@ export const styles = `
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param dt {
|
||||
color: var(--text-color-medium, #666);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param dd {
|
||||
@@ -423,6 +427,30 @@ export const styles = `
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
padding: 1px 8px;
|
||||
border: 1px solid var(--success-color-low, #2d7d46);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-type {
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__outbounds-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
@@ -431,52 +459,115 @@ export const styles = `
|
||||
.pdk_dashboard-page__urltest-details__table {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
width: calc(100% + 14px);
|
||||
box-sizing: border-box;
|
||||
max-height: min(46vh, 460px);
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 14px;
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(54px, max-content) 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px solid var(--border-color-low, #eee);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row--active {
|
||||
font-weight: 600;
|
||||
border-color: var(--success-color-low, #2d7d46);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name,
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name b {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name span,
|
||||
.pdk_dashboard-page__urltest-details__row-type,
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pdk_dashboard-page__urltest-details__row-type {
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
justify-content: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 20px;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-placeholder {
|
||||
display: block;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__empty {
|
||||
padding: 8px;
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.pdk_dashboard-page__urltest-details__params {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3239,6 +3239,18 @@ function sortOutboundsForDashboard(outbounds, options = {}) {
|
||||
return left.index - right.index;
|
||||
}).map((item) => item.outbound);
|
||||
}
|
||||
function sortUrlTestMembers(outbounds) {
|
||||
return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => {
|
||||
if (left.outbound.selected !== right.outbound.selected) {
|
||||
return left.outbound.selected ? -1 : 1;
|
||||
}
|
||||
const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound);
|
||||
if (latencyDiff !== 0) {
|
||||
return latencyDiff;
|
||||
}
|
||||
return left.index - right.index;
|
||||
}).map((item) => item.outbound);
|
||||
}
|
||||
function isSafeSectionName(sectionName) {
|
||||
return /^[A-Za-z0-9_-]+$/.test(sectionName);
|
||||
}
|
||||
@@ -3292,28 +3304,30 @@ function buildUrlTestInfo({
|
||||
groupCache?.outbounds?.length ? groupCache.outbounds : entry.value.all || []
|
||||
);
|
||||
const selectedCode = entry.value.now || "";
|
||||
const outbounds = childCodes.flatMap((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
return [
|
||||
{
|
||||
code: childCode,
|
||||
displayName: getOutboundDisplayName(
|
||||
childCode,
|
||||
childEntry,
|
||||
const outbounds = sortUrlTestMembers(
|
||||
childCodes.flatMap((childCode) => {
|
||||
const childEntry = proxyByCode.get(childCode);
|
||||
const link = manualLinkByCode.get(childCode) || "";
|
||||
const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode);
|
||||
return [
|
||||
{
|
||||
code: childCode,
|
||||
displayName: getOutboundDisplayName(
|
||||
childCode,
|
||||
childEntry,
|
||||
link,
|
||||
outboundMetadata
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
outboundMetadata
|
||||
),
|
||||
latency: childEntry?.value?.history?.[0]?.delay || 0,
|
||||
type: childEntry?.value?.type || "",
|
||||
selected: selectedCode === childCode,
|
||||
link,
|
||||
canCopyLink,
|
||||
country: showDetectedCountries ? outboundMetadata?.countries?.[childCode] : void 0
|
||||
}
|
||||
];
|
||||
});
|
||||
canCopyLink,
|
||||
country: showDetectedCountries ? outboundMetadata?.countries?.[childCode] : void 0
|
||||
}
|
||||
];
|
||||
})
|
||||
);
|
||||
const selectedName = outbounds.find((outbound) => outbound.code === selectedCode)?.displayName || selectedCode;
|
||||
return {
|
||||
code,
|
||||
@@ -5358,14 +5372,62 @@ function formatUrlTestModalValue(value) {
|
||||
return value ? _("Yes") : _("No");
|
||||
}
|
||||
const text = `${value ?? ""}`.trim();
|
||||
return text || _("N/A");
|
||||
return text || _("No");
|
||||
}
|
||||
function getUrlTestLatencyClass(latency) {
|
||||
if (!latency) {
|
||||
return "pdk_dashboard-page__outbound-grid__item__latency--empty";
|
||||
}
|
||||
if (latency < 800) {
|
||||
return "pdk_dashboard-page__outbound-grid__item__latency--green";
|
||||
}
|
||||
if (latency < 1500) {
|
||||
return "pdk_dashboard-page__outbound-grid__item__latency--yellow";
|
||||
}
|
||||
return "pdk_dashboard-page__outbound-grid__item__latency--red";
|
||||
}
|
||||
function formatUrlTestLatency(latency) {
|
||||
return latency ? `${latency}ms` : "N/A";
|
||||
}
|
||||
function renderUrlTestSelectedValue(info) {
|
||||
const selectedMember = info.outbounds.find((member) => member.selected);
|
||||
const selectedName = selectedMember?.displayName || info.selectedName || info.selectedCode || "";
|
||||
const name = formatUrlTestModalValue(selectedName);
|
||||
if (name === _("No")) {
|
||||
return E("span", {}, name);
|
||||
}
|
||||
return E(
|
||||
"span",
|
||||
{ class: "pdk_dashboard-page__urltest-details__selected-value" },
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
{ class: "pdk_dashboard-page__urltest-details__selected-name" },
|
||||
name
|
||||
),
|
||||
...selectedMember?.type ? [
|
||||
E(
|
||||
"span",
|
||||
{ class: "pdk_dashboard-page__urltest-details__selected-type" },
|
||||
selectedMember.type
|
||||
)
|
||||
] : [],
|
||||
...selectedMember ? [
|
||||
E(
|
||||
"span",
|
||||
{ class: getUrlTestLatencyClass(selectedMember.latency) },
|
||||
formatUrlTestLatency(selectedMember.latency)
|
||||
)
|
||||
] : []
|
||||
]
|
||||
);
|
||||
}
|
||||
function renderUrlTestCopyButton(title, onClick) {
|
||||
return E(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class: "btn pdk_dashboard-page__outbound-grid__item__copy-button",
|
||||
class: "btn pdk_dashboard-page__urltest-details__copy-button",
|
||||
title,
|
||||
"aria-label": title,
|
||||
click: onClick
|
||||
@@ -5379,29 +5441,31 @@ function renderUrlTestInfoModal(section, outbound) {
|
||||
return E("div", {}, _("URLTest details are unavailable"));
|
||||
}
|
||||
const fields = [
|
||||
[_("Selected"), info.selectedName || info.selectedCode],
|
||||
[_("Testing URL"), info.url, info.url],
|
||||
[_("Interval"), info.interval],
|
||||
[_("Tolerance"), info.tolerance],
|
||||
[_("Idle timeout"), info.idleTimeout],
|
||||
[_("Interrupt existing connections"), info.interruptExistConnections]
|
||||
{
|
||||
label: _("Selected"),
|
||||
children: [renderUrlTestSelectedValue(info)]
|
||||
},
|
||||
{ label: _("Testing URL"), value: info.url },
|
||||
{ label: _("Interval"), value: info.interval },
|
||||
{ label: _("Tolerance"), value: info.tolerance },
|
||||
{ label: _("Idle timeout"), value: info.idleTimeout },
|
||||
{
|
||||
label: _("Interrupt connections"),
|
||||
value: info.interruptExistConnections
|
||||
}
|
||||
];
|
||||
return E("div", { class: "pdk_dashboard-page__urltest-details" }, [
|
||||
E(
|
||||
"dl",
|
||||
{ class: "pdk_dashboard-page__urltest-details__params" },
|
||||
fields.map(
|
||||
([label, value, copyValue]) => E("div", { class: "pdk_dashboard-page__urltest-details__param" }, [
|
||||
({ label, value, children }) => E("div", { class: "pdk_dashboard-page__urltest-details__param" }, [
|
||||
E("dt", {}, label),
|
||||
E("dd", {}, [
|
||||
E("span", {}, formatUrlTestModalValue(value)),
|
||||
...copyValue ? [
|
||||
renderUrlTestCopyButton(_("Copy URL"), (event) => {
|
||||
event.preventDefault();
|
||||
copyToClipboard(copyValue);
|
||||
})
|
||||
] : []
|
||||
])
|
||||
E(
|
||||
"dd",
|
||||
{},
|
||||
children || [E("span", {}, formatUrlTestModalValue(value))]
|
||||
)
|
||||
])
|
||||
)
|
||||
),
|
||||
@@ -5431,7 +5495,15 @@ function renderUrlTestInfoModal(section, outbound) {
|
||||
},
|
||||
[
|
||||
E("b", {}, member.displayName),
|
||||
member.selected ? E("span", {}, _("Selected")) : ""
|
||||
...member.type ? [
|
||||
E(
|
||||
"span",
|
||||
{
|
||||
class: "pdk_dashboard-page__urltest-details__row-type"
|
||||
},
|
||||
member.type
|
||||
)
|
||||
] : []
|
||||
]
|
||||
),
|
||||
E(
|
||||
@@ -5440,26 +5512,41 @@ function renderUrlTestInfoModal(section, outbound) {
|
||||
class: "pdk_dashboard-page__urltest-details__row-meta"
|
||||
},
|
||||
[
|
||||
E("span", {}, member.type || _("N/A")),
|
||||
E(
|
||||
"span",
|
||||
{},
|
||||
member.latency ? `${member.latency}ms` : "N/A"
|
||||
{ class: getUrlTestLatencyClass(member.latency) },
|
||||
formatUrlTestLatency(member.latency)
|
||||
)
|
||||
]
|
||||
),
|
||||
...member.canCopyLink ? [
|
||||
renderUrlTestCopyButton(
|
||||
_("Copy proxy link"),
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
}
|
||||
)
|
||||
] : []
|
||||
member.canCopyLink ? renderUrlTestCopyButton(_("Copy proxy link"), (event) => {
|
||||
event.preventDefault();
|
||||
void handleCopyOutbound(section, member);
|
||||
}) : E("span", {
|
||||
class: "pdk_dashboard-page__urltest-details__copy-placeholder"
|
||||
})
|
||||
]
|
||||
)
|
||||
) : [E("div", {}, _("No outbounds"))]
|
||||
) : [
|
||||
E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__urltest-details__empty" },
|
||||
_("No outbounds")
|
||||
)
|
||||
]
|
||||
)
|
||||
]),
|
||||
E("div", { class: "pdk_dashboard-page__urltest-details__footer" }, [
|
||||
E(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class: "btn cbi-button cbi-button-neutral",
|
||||
click: () => {
|
||||
ui.hideModal();
|
||||
}
|
||||
},
|
||||
_("Close")
|
||||
)
|
||||
])
|
||||
]);
|
||||
@@ -5468,7 +5555,10 @@ function handleShowUrlTestInfo(section, outbound) {
|
||||
if (!outbound.urlTestInfo) {
|
||||
return;
|
||||
}
|
||||
ui.showModal(_("URLTest details"), renderUrlTestInfoModal(section, outbound));
|
||||
ui.showModal(
|
||||
`${_("URLTest details")}: ${outbound.urlTestInfo.displayName || outbound.displayName}`,
|
||||
renderUrlTestInfoModal(section, outbound)
|
||||
);
|
||||
}
|
||||
async function handleUpdateSubscription(section) {
|
||||
if (store.get().sectionsWidget.subscriptionUpdatingSections[section.sectionName]) {
|
||||
@@ -6226,14 +6316,17 @@ var styles = `
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details {
|
||||
min-width: min(720px, 86vw);
|
||||
box-sizing: border-box;
|
||||
width: min(760px, calc(100vw - 56px));
|
||||
max-width: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__params {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, max-content) minmax(0, 1fr);
|
||||
gap: 8px 16px;
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param {
|
||||
@@ -6242,6 +6335,7 @@ var styles = `
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param dt {
|
||||
color: var(--text-color-medium, #666);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__param dd {
|
||||
@@ -6257,6 +6351,30 @@ var styles = `
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
padding: 1px 8px;
|
||||
border: 1px solid var(--success-color-low, #2d7d46);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__selected-type {
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__outbounds-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
@@ -6265,52 +6383,115 @@ var styles = `
|
||||
.pdk_dashboard-page__urltest-details__table {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
width: calc(100% + 14px);
|
||||
box-sizing: border-box;
|
||||
max-height: min(46vh, 460px);
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 14px;
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(54px, max-content) 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px solid var(--border-color-low, #eee);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row--active {
|
||||
font-weight: 600;
|
||||
border-color: var(--success-color-low, #2d7d46);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name,
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name b {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-name span,
|
||||
.pdk_dashboard-page__urltest-details__row-type,
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pdk_dashboard-page__urltest-details__row-type {
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
justify-content: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 20px;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-button svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__copy-placeholder {
|
||||
display: block;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__empty {
|
||||
padding: 8px;
|
||||
color: var(--text-color-medium, #666);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.pdk_dashboard-page__urltest-details__params {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1fr) 20px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__urltest-details__row-meta {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-02 17:43+0300\n"
|
||||
"PO-Revision-Date: 2026-07-02 17:43+0300\n"
|
||||
"POT-Creation-Date: 2026-07-02 23:54+0300\n"
|
||||
"PO-Revision-Date: 2026-07-02 23:54+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -281,9 +281,6 @@ msgstr "Копировать"
|
||||
msgid "Copy proxy link"
|
||||
msgstr "Скопировать ссылку прокси"
|
||||
|
||||
msgid "Copy URL"
|
||||
msgstr "Скопировать URL"
|
||||
|
||||
msgid "Could not compare FakeIP and control public IPs"
|
||||
msgstr "Не удалось сравнить публичные IP FakeIP и контрольной проверки"
|
||||
|
||||
@@ -623,8 +620,8 @@ msgstr "Задержка при мониторинге интерфейсов"
|
||||
msgid "Interface monitoring for Bad WAN"
|
||||
msgstr "Мониторинг интерфейса для Bad WAN"
|
||||
|
||||
msgid "Interrupt existing connections"
|
||||
msgstr "Прерывать существующие соединения"
|
||||
msgid "Interrupt connections"
|
||||
msgstr "Прерывать соединения"
|
||||
|
||||
msgid "Interval"
|
||||
msgstr "Интервал"
|
||||
@@ -911,9 +908,6 @@ msgstr "Мониторинг"
|
||||
msgid "Must be a number in the range of 50 - 1000"
|
||||
msgstr "Должно быть числом от 50 до 1000"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "Н/Д"
|
||||
|
||||
msgid "Network Interface"
|
||||
msgstr "Сетевой интерфейс"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2219,7 +2219,7 @@ function xray_tag_base(original_tag, config_index, config_count) {
|
||||
original_tag = as_string(original_tag);
|
||||
if (original_tag == "")
|
||||
original_tag = "server";
|
||||
return config_count > 1 ? ("xray-" + (config_index + 1) + "-" + original_tag) : original_tag;
|
||||
return original_tag;
|
||||
}
|
||||
|
||||
function xray_protocol_supported(protocol) {
|
||||
|
||||
@@ -135,6 +135,9 @@ if (group.interval != "45s")
|
||||
die("xray urltest interval was not preserved\n");
|
||||
if (group.tolerance != null)
|
||||
die("xray urltest should not receive hardcoded tolerance\n");
|
||||
for (let child in group.outbounds || [])
|
||||
if (substr(child, 0, 5) == "xray-")
|
||||
die("xray urltest child tags should preserve source names when unique\n");
|
||||
' "$xray_normalized" || fail "xray normalized URLTest fields"
|
||||
|
||||
prepare_subscription_cache proxy 1 "https://xray.example/sub" "$xray_normalized"
|
||||
@@ -186,6 +189,9 @@ if (!builtin || length(builtin.outbounds || []) != 2)
|
||||
for (let child in builtin.outbounds || [])
|
||||
if (child == "Latvia group")
|
||||
die("built-in URLTest must not use the xray group tag as a child\n");
|
||||
for (let child in builtin.outbounds || [])
|
||||
if (substr(child, 0, 5) == "xray-")
|
||||
die("built-in URLTest must not use artificial xray child tag prefixes\n");
|
||||
if (object_or_empty(cache.urltestGroups)["Latvia group"].url != "https://probe.example/204")
|
||||
die("section cache is missing imported xray URLTest params\n");
|
||||
if (length(object_or_empty(cache.urltestGroups)["proxy-urltest-out"].outbounds || []) != 2)
|
||||
|
||||
Reference in New Issue
Block a user