fix(dashboard): prevent service sections from falling back to proxy outbound view and showing latency test

This commit is contained in:
Dushnilin
2026-09-07 12:19:21 +03:00
parent 5710ab6127
commit be9844d898
5 changed files with 798 additions and 433 deletions
@@ -1473,6 +1473,76 @@ export async function getDashboardSections(
value,
}),
);
const serviceStatusCache = new Map<
string,
Promise<Tachyon.ServiceStatus | undefined>
>();
const getServiceStatus = (serviceType: 'zapret' | 'zapret2' | 'byedpi') => {
if (!serviceStatusCache.has(serviceType)) {
serviceStatusCache.set(
serviceType,
(async () => {
try {
if (serviceType === 'zapret') {
const result = await TachyonShellMethods.getZapretStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'zapret',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
} else if (serviceType === 'zapret2') {
const result = await TachyonShellMethods.getZapret2Status();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'zapret2',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: 0,
unstable: false,
statusMessage: s.status_message,
};
}
} else if (serviceType === 'byedpi') {
const result = await TachyonShellMethods.getByedpiStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'byedpi',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
}
} catch (_error) {
// Status fetch failed — show section without status
}
return undefined;
})(),
);
}
return serviceStatusCache.get(serviceType)!;
};
const data = await Promise.all(
configSections
.filter(
@@ -1599,62 +1669,7 @@ export async function getDashboardSections(
if (isServiceAction(sectionAction)) {
const serviceType = sectionAction as 'zapret' | 'zapret2' | 'byedpi';
let serviceStatus: Tachyon.ServiceStatus | undefined;
try {
if (serviceType === 'zapret') {
const result = await TachyonShellMethods.getZapretStatus();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: 'zapret',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
} else if (serviceType === 'zapret2') {
const result = await TachyonShellMethods.getZapret2Status();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: 'zapret2',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: 0,
unstable: false,
statusMessage: s.status_message,
};
}
} else if (serviceType === 'byedpi') {
const result = await TachyonShellMethods.getByedpiStatus();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: 'byedpi',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
}
} catch (_error) {
// Status fetch failed — show section without status
}
const serviceStatus = await getServiceStatus(serviceType);
const statusLabel = serviceStatus
? serviceStatus.ready
@@ -277,9 +277,142 @@ function renderDefaultState({
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(
section.action || '',
);
const isServiceNode = ['zapret', 'zapret2', 'byedpi'].includes(
section.action || '',
);
const isServiceNode =
['zapret', 'zapret2', 'byedpi'].includes(section.action || '') ||
Boolean(section.serviceStatus);
if (isServiceNode) {
const ss = section.serviceStatus;
const serviceType =
ss?.serviceType ||
(['zapret', 'zapret2', 'byedpi'].includes(section.action || '')
? (section.action as 'zapret' | 'zapret2' | 'byedpi')
: 'zapret');
const typeLabel =
serviceType === 'zapret'
? 'Zapret'
: serviceType === 'zapret2'
? 'Zapret2'
: 'ByeDPI';
const statusColor = ss
? ss.ready
? 'var(--success-color-medium, green)'
: ss.conflict
? 'var(--error-color-medium, red)'
: ss.configured
? 'var(--warn-color-medium, orange)'
: 'var(--primary-color-low, lightgray)'
: 'var(--primary-color-low, lightgray)';
const statusText = ss
? ss.ready
? _('Running')
: ss.conflict
? _('Conflict')
: ss.configured
? _('Stopped')
: _('Not configured')
: _('Unknown');
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: default;',
},
[
E(
'div',
{
class:
'tachyon_dashboard-page__outbound-section__title-section__title',
style: 'display: flex; align-items: center; gap: 8px;',
},
[
E('span', {}, section.displayName),
E(
'span',
{
style: 'font-size: 12px; opacity: 0.6; font-weight: normal;',
},
typeLabel,
),
],
),
],
),
E(
'div',
{
style:
'display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;',
},
[
E('div', { style: 'display: flex; align-items: center; gap: 6px;' }, [
E(
'span',
{ style: 'opacity: 0.7; font-size: 13px;' },
_('Status') + ':',
),
E(
'span',
{
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`,
},
statusText,
),
]),
ss && ss.restartCount > 0
? E(
'div',
{ style: 'display: flex; align-items: center; gap: 6px;' },
[
E(
'span',
{ style: 'opacity: 0.7; font-size: 13px;' },
_('Restarts') + ':',
),
E(
'span',
{
style:
'font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);',
},
`${ss.restartCount}`,
),
],
)
: '',
ss && ss.unstable
? E(
'div',
{ style: 'display: flex; align-items: center; gap: 6px;' },
[
E(
'span',
{
style:
'font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);',
},
_('Unstable'),
),
],
)
: '',
],
),
ss?.statusMessage
? E(
'div',
{
style:
'padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;',
},
formatServiceStatusMessage(ss.statusMessage),
)
: '',
]);
}
function testLatency() {
if (section.withTagSelect) {
@@ -606,130 +739,6 @@ function renderDefaultState({
);
}
if (isServiceNode && section.serviceStatus) {
const ss = section.serviceStatus;
const statusColor = ss.ready
? 'var(--success-color-medium, green)'
: ss.conflict
? 'var(--error-color-medium, red)'
: ss.configured
? 'var(--warn-color-medium, orange)'
: 'var(--primary-color-low, lightgray)';
const statusText = ss.ready
? _('Running')
: ss.conflict
? _('Conflict')
: ss.configured
? _('Stopped')
: _('Not configured');
const typeLabel =
ss.serviceType === 'zapret'
? 'Zapret'
: ss.serviceType === 'zapret2'
? 'Zapret2'
: 'ByeDPI';
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: default;',
},
[
E(
'div',
{
class:
'tachyon_dashboard-page__outbound-section__title-section__title',
style: 'display: flex; align-items: center; gap: 8px;',
},
[
E('span', {}, section.displayName),
E(
'span',
{
style: 'font-size: 12px; opacity: 0.6; font-weight: normal;',
},
typeLabel,
),
],
),
],
),
E(
'div',
{
style:
'display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;',
},
[
E('div', { style: 'display: flex; align-items: center; gap: 6px;' }, [
E(
'span',
{ style: 'opacity: 0.7; font-size: 13px;' },
_('Status') + ':',
),
E(
'span',
{
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`,
},
statusText,
),
]),
ss.restartCount > 0
? E(
'div',
{ style: 'display: flex; align-items: center; gap: 6px;' },
[
E(
'span',
{ style: 'opacity: 0.7; font-size: 13px;' },
_('Restarts') + ':',
),
E(
'span',
{
style:
'font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);',
},
`${ss.restartCount}`,
),
],
)
: '',
ss.unstable
? E(
'div',
{ style: 'display: flex; align-items: center; gap: 6px;' },
[
E(
'span',
{
style:
'font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);',
},
_('Unstable'),
),
],
)
: '',
],
),
ss.statusMessage
? E(
'div',
{
style:
'padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;',
},
formatServiceStatusMessage(ss.statusMessage),
)
: '',
]);
}
const metadataNodes = (section.subscriptionMetadata || [])
.map((metadata) => renderSubscriptionMetadata(metadata))
.filter(Boolean) as HTMLElement[];
@@ -739,6 +748,11 @@ function renderDefaultState({
onUpdateSubscription,
);
const canTestLatency =
!isServiceNode &&
(section.withTagSelect ||
(section.outbounds && section.outbounds.length > 0));
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
// Title with test latency
E(
@@ -867,47 +881,52 @@ function renderDefaultState({
},
[
...(subscriptionUpdateAction ? [subscriptionUpdateAction] : []),
E(
'button',
{
type: 'button',
class: 'btn dashboard-sections-grid-item-test-latency',
'data-latency-section': section.sectionName,
disabled: latencyFetching ? true : undefined,
click: (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) {
return;
}
testLatency();
},
},
latencyFetching
? [
renderLoaderCircleIcon24(),
E(
'span',
{
class:
'dashboard-sections-grid-item-test-latency__label',
},
isConnectionNode
? _('Checking...')
: getLatencyTestLabel(latencyProgress),
),
]
: E(
'span',
...(canTestLatency
? [
E(
'button',
{
class: 'dashboard-sections-grid-item-test-latency__label',
type: 'button',
class: 'btn dashboard-sections-grid-item-test-latency',
'data-latency-section': section.sectionName,
disabled: latencyFetching ? true : undefined,
click: (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) {
return;
}
testLatency();
},
},
isConnectionNode
? _('Check Connection')
: _('Test latency'),
latencyFetching
? [
renderLoaderCircleIcon24(),
E(
'span',
{
class:
'dashboard-sections-grid-item-test-latency__label',
},
isConnectionNode
? _('Checking...')
: getLatencyTestLabel(latencyProgress),
),
]
: E(
'span',
{
class:
'dashboard-sections-grid-item-test-latency__label',
},
isConnectionNode
? _('Check Connection')
: _('Test latency'),
),
),
),
]
: []),
],
),
],
@@ -0,0 +1,300 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
class MockMutationObserver {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
}
globalThis.MutationObserver = MockMutationObserver as any;
function createDummyElement(tag: string) {
const children: any[] = [];
return {
tagName: tag.toUpperCase(),
children,
childNodes: children,
style: {} as Record<string, string>,
className: '',
textContent: '',
setAttribute(name: string, val: string) {
(this as any)[name] = val;
},
getAttribute(name: string) {
return (this as any)[name] ?? null;
},
querySelectorAll(selector: string): any[] {
const results: any[] = [];
function traverse(node: any) {
if (!node || typeof node !== 'object') return;
if (selector.startsWith('.')) {
const targetClass = selector.slice(1);
const classes = (node.class || node.className || '').split(/\s+/);
if (classes.includes(targetClass)) {
results.push(node);
}
}
if (selector === 'button' && node.tagName === 'BUTTON') {
results.push(node);
}
if (selector === 'svg' && node.tagName === 'SVG') {
results.push(node);
}
if (Array.isArray(node.children)) {
for (const child of node.children) {
traverse(child);
}
}
}
traverse(this);
return results;
},
querySelector(selector: string): any {
return this.querySelectorAll(selector)[0] || null;
},
appendChild(node: any) {
children.push(node);
},
};
}
globalThis.document = {
body: createDummyElement('body') as any,
createElement: (tag: string) => createDummyElement(tag) as any,
createElementNS: (_ns: string, tag: string) => createDummyElement(tag) as any,
querySelectorAll: () => [],
querySelector: () => null,
getElementById: () => null,
} as any;
(globalThis as any).rpc = { declare: vi.fn() };
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
(globalThis as any).E = (tag: string, attrs?: any, children?: any) => {
const el = (globalThis as any).document.createElement(tag);
if (attrs) {
Object.assign(el, attrs);
}
if (children) {
const list = Array.isArray(children) ? children : [children];
for (const child of list) {
if (child !== null && child !== undefined && child !== '') {
el.appendChild(child);
}
}
}
return el;
};
let renderSections: any;
describe('renderSections', () => {
beforeAll(async () => {
const mod = await import('../renderSections');
renderSections = mod.renderSections;
});
beforeEach(() => {
vi.clearAllMocks();
});
it('renders service section without serviceStatus as a clean service card without latency button', () => {
const section: any = {
code: 'discord',
sectionName: 'discord',
displayName: 'Discord',
action: 'zapret2',
withTagSelect: false,
serviceStatus: undefined,
outbounds: [
{
code: 'discord',
displayName: 'Unknown',
latency: 0,
type: 'ZAPRET2',
selected: true,
canCopyLink: false,
},
],
};
const el = renderSections({
loading: false,
failed: false,
section,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const latencyButtons = el.querySelectorAll(
'.dashboard-sections-grid-item-test-latency',
);
expect(latencyButtons).toHaveLength(0);
const buttons = el.querySelectorAll('button');
expect(buttons).toHaveLength(0);
// Should not render chevron svg
const svgs = el.querySelectorAll('svg');
expect(svgs).toHaveLength(0);
// Should not render outbound grid
const outboundGrid = el.querySelectorAll(
'.tachyon_dashboard-page__outbound-grid',
);
expect(outboundGrid).toHaveLength(0);
});
it('renders service section with serviceStatus as a service card without latency button', () => {
const section: any = {
code: 'youtube',
sectionName: 'youtube',
displayName: 'Youtube',
action: 'zapret2',
withTagSelect: false,
serviceStatus: {
serviceType: 'zapret2',
configured: true,
ready: true,
conflict: false,
runningProcesses: 1,
expectedProcesses: 1,
restartCount: 0,
unstable: false,
statusMessage: 'zapret2 provider status is normal',
},
outbounds: [],
};
const el = renderSections({
loading: false,
failed: false,
section,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const latencyButtons = el.querySelectorAll(
'.dashboard-sections-grid-item-test-latency',
);
expect(latencyButtons).toHaveLength(0);
const buttons = el.querySelectorAll('button');
expect(buttons).toHaveLength(0);
});
it('renders zapret and byedpi service sections without latency button', () => {
for (const action of ['zapret', 'byedpi']) {
const section: any = {
code: action,
sectionName: action,
displayName: action.toUpperCase(),
action,
withTagSelect: false,
outbounds: [],
};
const el = renderSections({
loading: false,
failed: false,
section,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const latencyButtons = el.querySelectorAll(
'.dashboard-sections-grid-item-test-latency',
);
expect(latencyButtons).toHaveLength(0);
}
});
it('renders latency test button for proxy outbound sections with outbounds', () => {
const section: any = {
code: 'proxy-group',
sectionName: 'main',
displayName: 'Main Proxy',
action: 'connection',
withTagSelect: true,
outbounds: [
{
code: 'node-1',
displayName: 'Node 1',
latency: 120,
type: 'VLESS',
selected: true,
},
],
};
const el = renderSections({
loading: false,
failed: false,
section,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const latencyButtons = el.querySelectorAll(
'.dashboard-sections-grid-item-test-latency',
);
expect(latencyButtons).toHaveLength(1);
});
it('does not render latency test button for empty proxy section', () => {
const section: any = {
code: 'empty-group',
sectionName: 'empty',
displayName: 'Empty Group',
action: 'connection',
withTagSelect: false,
outbounds: [],
};
const el = renderSections({
loading: false,
failed: false,
section,
onTestLatency: vi.fn(),
onChooseOutbound: vi.fn(),
onCopyOutbound: vi.fn(),
onShowUrlTestInfo: vi.fn(),
onShowPriorityInfo: vi.fn(),
onUpdateSubscription: vi.fn(),
latencyFetching: false,
subscriptionUpdating: false,
});
const latencyButtons = el.querySelectorAll(
'.dashboard-sections-grid-item-test-latency',
);
expect(latencyButtons).toHaveLength(0);
});
});
@@ -127,29 +127,39 @@ export async function runSectionsCheck() {
// Service sections (zapret/zapret2/byedpi) don't have Clash API proxies.
// Use the serviceStatus that was already fetched in getDashboardSections.
if (section.serviceStatus) {
const s = section.serviceStatus;
if (s.ready) {
return {
state: 'success',
latency: _('Running'),
};
}
if (s.conflict) {
const isService =
['zapret', 'zapret2', 'byedpi'].includes(section.action || '') ||
Boolean(section.serviceStatus);
if (isService) {
if (section.serviceStatus) {
const s = section.serviceStatus;
if (s.ready) {
return {
state: 'success',
latency: _('Running'),
};
}
if (s.conflict) {
return {
state: 'error',
latency: _('Conflict'),
};
}
if (s.configured) {
return {
state: 'warning',
latency: _('Stopped'),
};
}
return {
state: 'error',
latency: _('Conflict'),
};
}
if (s.configured) {
return {
state: 'warning',
latency: _('Stopped'),
latency: _('Not configured'),
};
}
return {
state: 'error',
latency: _('Not configured'),
state: 'warning',
latency: _('Unknown'),
};
}
@@ -2402,9 +2402,102 @@ function renderDefaultState({
const isConnectionNode = ["vpn", "awg", "warp"].includes(
section.action || ""
);
const isServiceNode = ["zapret", "zapret2", "byedpi"].includes(
section.action || ""
);
const isServiceNode = ["zapret", "zapret2", "byedpi"].includes(section.action || "") || Boolean(section.serviceStatus);
if (isServiceNode) {
const ss = section.serviceStatus;
const serviceType = ss?.serviceType || (["zapret", "zapret2", "byedpi"].includes(section.action || "") ? section.action : "zapret");
const typeLabel = serviceType === "zapret" ? "Zapret" : serviceType === "zapret2" ? "Zapret2" : "ByeDPI";
const statusColor = ss ? ss.ready ? "var(--success-color-medium, green)" : ss.conflict ? "var(--error-color-medium, red)" : ss.configured ? "var(--warn-color-medium, orange)" : "var(--primary-color-low, lightgray)" : "var(--primary-color-low, lightgray)";
const statusText = ss ? ss.ready ? _("Running") : ss.conflict ? _("Conflict") : ss.configured ? _("Stopped") : _("Not configured") : _("Unknown");
return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: default;"
},
[
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section__title",
style: "display: flex; align-items: center; gap: 8px;"
},
[
E("span", {}, section.displayName),
E(
"span",
{
style: "font-size: 12px; opacity: 0.6; font-weight: normal;"
},
typeLabel
)
]
)
]
),
E(
"div",
{
style: "display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;"
},
[
E("div", { style: "display: flex; align-items: center; gap: 6px;" }, [
E(
"span",
{ style: "opacity: 0.7; font-size: 13px;" },
_("Status") + ":"
),
E(
"span",
{
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`
},
statusText
)
]),
ss && ss.restartCount > 0 ? E(
"div",
{ style: "display: flex; align-items: center; gap: 6px;" },
[
E(
"span",
{ style: "opacity: 0.7; font-size: 13px;" },
_("Restarts") + ":"
),
E(
"span",
{
style: "font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);"
},
`${ss.restartCount}`
)
]
) : "",
ss && ss.unstable ? E(
"div",
{ style: "display: flex; align-items: center; gap: 6px;" },
[
E(
"span",
{
style: "font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);"
},
_("Unstable")
)
]
) : ""
]
),
ss?.statusMessage ? E(
"div",
{
style: "padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;"
},
formatServiceStatusMessage(ss.statusMessage)
) : ""
]);
}
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(
@@ -2653,106 +2746,13 @@ function renderDefaultState({
]
);
}
if (isServiceNode && section.serviceStatus) {
const ss = section.serviceStatus;
const statusColor = ss.ready ? "var(--success-color-medium, green)" : ss.conflict ? "var(--error-color-medium, red)" : ss.configured ? "var(--warn-color-medium, orange)" : "var(--primary-color-low, lightgray)";
const statusText = ss.ready ? _("Running") : ss.conflict ? _("Conflict") : ss.configured ? _("Stopped") : _("Not configured");
const typeLabel = ss.serviceType === "zapret" ? "Zapret" : ss.serviceType === "zapret2" ? "Zapret2" : "ByeDPI";
return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: default;"
},
[
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section__title",
style: "display: flex; align-items: center; gap: 8px;"
},
[
E("span", {}, section.displayName),
E(
"span",
{
style: "font-size: 12px; opacity: 0.6; font-weight: normal;"
},
typeLabel
)
]
)
]
),
E(
"div",
{
style: "display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;"
},
[
E("div", { style: "display: flex; align-items: center; gap: 6px;" }, [
E(
"span",
{ style: "opacity: 0.7; font-size: 13px;" },
_("Status") + ":"
),
E(
"span",
{
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`
},
statusText
)
]),
ss.restartCount > 0 ? E(
"div",
{ style: "display: flex; align-items: center; gap: 6px;" },
[
E(
"span",
{ style: "opacity: 0.7; font-size: 13px;" },
_("Restarts") + ":"
),
E(
"span",
{
style: "font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);"
},
`${ss.restartCount}`
)
]
) : "",
ss.unstable ? E(
"div",
{ style: "display: flex; align-items: center; gap: 6px;" },
[
E(
"span",
{
style: "font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);"
},
_("Unstable")
)
]
) : ""
]
),
ss.statusMessage ? E(
"div",
{
style: "padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;"
},
formatServiceStatusMessage(ss.statusMessage)
) : ""
]);
}
const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean);
const subscriptionUpdateAction = renderSubscriptionUpdateAction(
section,
subscriptionUpdating,
onUpdateSubscription
);
const canTestLatency = !isServiceNode && (section.withTagSelect || section.outbounds && section.outbounds.length > 0);
return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [
// Title with test latency
E(
@@ -2854,39 +2854,41 @@ function renderDefaultState({
},
[
...subscriptionUpdateAction ? [subscriptionUpdateAction] : [],
E(
"button",
{
type: "button",
class: "btn dashboard-sections-grid-item-test-latency",
"data-latency-section": section.sectionName,
disabled: latencyFetching ? true : void 0,
click: (event) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) {
return;
...canTestLatency ? [
E(
"button",
{
type: "button",
class: "btn dashboard-sections-grid-item-test-latency",
"data-latency-section": section.sectionName,
disabled: latencyFetching ? true : void 0,
click: (event) => {
event.preventDefault();
event.stopPropagation();
if (latencyFetching) {
return;
}
testLatency();
}
testLatency();
}
},
latencyFetching ? [
renderLoaderCircleIcon24(),
E(
},
latencyFetching ? [
renderLoaderCircleIcon24(),
E(
"span",
{
class: "dashboard-sections-grid-item-test-latency__label"
},
isConnectionNode ? _("Checking...") : getLatencyTestLabel(latencyProgress)
)
] : E(
"span",
{
class: "dashboard-sections-grid-item-test-latency__label"
},
isConnectionNode ? _("Checking...") : getLatencyTestLabel(latencyProgress)
isConnectionNode ? _("Check Connection") : _("Test latency")
)
] : E(
"span",
{
class: "dashboard-sections-grid-item-test-latency__label"
},
isConnectionNode ? _("Check Connection") : _("Test latency")
)
)
] : []
]
)
]
@@ -5594,6 +5596,70 @@ async function getDashboardSections(options = {}) {
value
})
);
const serviceStatusCache = /* @__PURE__ */ new Map();
const getServiceStatus = (serviceType) => {
if (!serviceStatusCache.has(serviceType)) {
serviceStatusCache.set(
serviceType,
(async () => {
try {
if (serviceType === "zapret") {
const result = await TachyonShellMethods.getZapretStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: "zapret",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
} else if (serviceType === "zapret2") {
const result = await TachyonShellMethods.getZapret2Status();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: "zapret2",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: 0,
unstable: false,
statusMessage: s.status_message
};
}
} else if (serviceType === "byedpi") {
const result = await TachyonShellMethods.getByedpiStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: "byedpi",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
}
} catch (_error) {
}
return void 0;
})()
);
}
return serviceStatusCache.get(serviceType);
};
const data = await Promise.all(
configSections.filter(
(section) => section.enabled !== "0" && (isConnectionAction(section.action) || isServiceAction(section.action))
@@ -5682,59 +5748,7 @@ async function getDashboardSections(options = {}) {
}
if (isServiceAction(sectionAction)) {
const serviceType = sectionAction;
let serviceStatus;
try {
if (serviceType === "zapret") {
const result = await TachyonShellMethods.getZapretStatus();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: "zapret",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
} else if (serviceType === "zapret2") {
const result = await TachyonShellMethods.getZapret2Status();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: "zapret2",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: 0,
unstable: false,
statusMessage: s.status_message
};
}
} else if (serviceType === "byedpi") {
const result = await TachyonShellMethods.getByedpiStatus();
if (result.success) {
const s = result.data;
serviceStatus = {
serviceType: "byedpi",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
}
} catch (_error) {
}
const serviceStatus = await getServiceStatus(serviceType);
const statusLabel = serviceStatus ? serviceStatus.ready ? _("Running") : serviceStatus.conflict ? _("Conflict") : serviceStatus.configured ? _("Stopped") : _("Not configured") : _("Unknown");
return {
withTagSelect: false,
@@ -14768,29 +14782,36 @@ async function runSectionsCheck() {
latency: _("Not responding")
};
}
if (section.serviceStatus) {
const s = section.serviceStatus;
if (s.ready) {
return {
state: "success",
latency: _("Running")
};
}
if (s.conflict) {
const isService = ["zapret", "zapret2", "byedpi"].includes(section.action || "") || Boolean(section.serviceStatus);
if (isService) {
if (section.serviceStatus) {
const s = section.serviceStatus;
if (s.ready) {
return {
state: "success",
latency: _("Running")
};
}
if (s.conflict) {
return {
state: "error",
latency: _("Conflict")
};
}
if (s.configured) {
return {
state: "warning",
latency: _("Stopped")
};
}
return {
state: "error",
latency: _("Conflict")
};
}
if (s.configured) {
return {
state: "warning",
latency: _("Stopped")
latency: _("Not configured")
};
}
return {
state: "error",
latency: _("Not configured")
state: "warning",
latency: _("Unknown")
};
}
const selectedOutbound = section.outbounds[0];