feat: add FPTN section action UI, config fields, dashboard status, and diagnostics

- Add FPTN to ROUTING_ACTIONS, actionProvidersAvailabilityState, isFptnInstalledForUi()
- Add FPTN action option label (FPTN) in getActionOptionLabel and getRuleActionDisplayValue
- Add FPTN to populateActionOptionValues (guarded by isFptnInstalledForUi())
- Add FPTN section config fields: access_token (required), sni, bypass_method, preferred_server
- Add fptn to ACTION_COLORS (#e74c3c)
- Add GetFptnStatus type, GET_FPTN_STATUS method, getFptnStatus() shell method
- Add fptn to dashboard: getDashboardSections, renderSections, getServiceStatus handler
- Add get_fptn_status CLI dispatch and get-fptn-status handler in diagnostics/runtime.uc
- Add FPTN unit test in getDashboardSections.test.ts
- Update translation calls index (locales/calls.json)
This commit is contained in:
Dushnilin
2026-09-15 21:25:54 +03:00
parent e6387aff25
commit 8855179793
11 changed files with 1959 additions and 1697 deletions
File diff suppressed because it is too large Load Diff
@@ -404,6 +404,7 @@ const ACTION_DISPLAY_NAMES: Record<string, string> = {
sudoku: 'Sudoku',
masque: 'MASQUE',
openvpn: 'OpenVPN',
fptn: 'FPTN',
};
function isConnectionAction(action?: string) {
@@ -417,7 +418,9 @@ function isConnectionAction(action?: string) {
function isServiceAction(action?: string) {
return Boolean(
action &&
['zapret', 'zapret2', 'byedpi', 'wdtt', 'olcrtc'].includes(action),
['zapret', 'zapret2', 'byedpi', 'wdtt', 'olcrtc', 'fptn'].includes(
action,
),
);
}
@@ -1606,7 +1609,7 @@ export async function getDashboardSections(
>();
const getServiceStatus = (
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc',
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc' | 'fptn',
) => {
if (!serviceStatusCache.has(serviceType)) {
serviceStatusCache.set(
@@ -1693,6 +1696,23 @@ export async function getDashboardSections(
statusMessage: s.status_message,
};
}
} else if (serviceType === 'fptn') {
const result = await TachyonShellMethods.getFptnStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'fptn',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: false,
runningProcesses: s.service_running ? 1 : 0,
expectedProcesses: s.configured ? 1 : 0,
restartCount: 0,
unstable: false,
statusMessage:
s.status_message || (s.ready ? _('Running') : _('Stopped')),
};
}
}
} catch (_error) {
// Status fetch failed — show section without status
@@ -1829,7 +1849,8 @@ export async function getDashboardSections(
}
if (isServiceAction(sectionAction)) {
const serviceType = sectionAction as 'zapret' | 'zapret2' | 'byedpi';
const serviceType =
sectionAction as Tachyon.ServiceStatus['serviceType'];
const serviceStatus = await getServiceStatus(serviceType);
const statusLabel = serviceStatus
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
getConfigSections: vi.fn(),
getClashApiProxies: vi.fn(),
getFptnStatus: vi.fn(),
canUseDirectClashApi: vi.fn(),
fsRead: vi.fn(),
}));
@@ -14,6 +15,7 @@ vi.mock('../getConfigSections', () => ({
vi.mock('../../shell', () => ({
TachyonShellMethods: {
getClashApiProxies: mocks.getClashApiProxies,
getFptnStatus: mocks.getFptnStatus,
},
}));
@@ -1306,4 +1308,36 @@ describe('getDashboardSections', () => {
expect(outbound?.prefix).toBe('Geodema Network');
expect(outbound?.displayName).toBe('🇫🇷 France');
});
it('returns FPTN service section with running status on dashboard', async () => {
mocks.getConfigSections.mockResolvedValue([
{
'.name': 'fptn_sec',
'.type': 'section',
enabled: '1',
action: 'fptn',
access_token: 'test_token',
},
]);
mocks.getFptnStatus.mockResolvedValue({
success: true,
data: {
installed: true,
configured: true,
service_running: true,
ready: true,
status_message: 'FPTN is running',
},
});
const result = await getDashboardSections();
expect(result.success).toBe(true);
expect(result.data).toHaveLength(1);
expect(result.data[0].sectionName).toBe('fptn_sec');
expect(result.data[0].action).toBe('fptn');
expect(result.data[0].serviceStatus?.serviceType).toBe('fptn');
expect(result.data[0].serviceStatus?.ready).toBe(true);
expect(result.data[0].outbounds[0].displayName).toBe('Running');
});
});
@@ -330,6 +330,13 @@ export const TachyonShellMethods = {
'/usr/bin/tachyon',
{ allowNonZeroWithStdout: true },
),
getFptnStatus: async () =>
callBaseMethod<Tachyon.GetFptnStatus>(
Tachyon.AvailableMethods.GET_FPTN_STATUS,
[],
'/usr/bin/tachyon',
{ allowNonZeroWithStdout: true },
),
getClashApiProxies: async () =>
callBaseMethod<ClashAPI.Proxies>(Tachyon.AvailableMethods.CLASH_API, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES,
@@ -294,22 +294,31 @@ function renderDefaultState({
'openvpn',
].includes(section.action || '');
const isServiceNode =
['zapret', 'zapret2', 'byedpi'].includes(section.action || '') ||
Boolean(section.serviceStatus);
['zapret', 'zapret2', 'byedpi', 'wdtt', 'olcrtc', 'fptn'].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', 'zapret2', 'byedpi', 'wdtt', 'olcrtc', 'fptn'].includes(
section.action || '',
)
? (section.action as Tachyon.ServiceStatus['serviceType'])
: 'zapret');
const typeLabel =
serviceType === 'zapret'
? 'Zapret'
: serviceType === 'zapret2'
? 'Zapret2'
: 'ByeDPI';
: serviceType === 'byedpi'
? 'ByeDPI'
: serviceType === 'wdtt'
? 'WDTT'
: serviceType === 'olcrtc'
? 'OlcRTC'
: 'FPTN';
const statusColor = ss
? ss.ready
? 'var(--success-color-medium, green)'
+23 -1
View File
@@ -71,6 +71,7 @@ export namespace Tachyon {
GET_BYEDPI_STATUS = 'get_byedpi_status',
GET_WDTT_STATUS = 'get_wdtt_status',
GET_OLCRTC_STATUS = 'get_olcrtc_status',
GET_FPTN_STATUS = 'get_fptn_status',
CLASH_API = 'clash_api',
ENABLE = 'enable',
DISABLE = 'disable',
@@ -204,7 +205,7 @@ export namespace Tachyon {
}
export interface ServiceStatus {
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc';
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc' | 'fptn';
configured: boolean;
ready: boolean;
conflict: boolean;
@@ -281,6 +282,7 @@ export namespace Tachyon {
| 'byedpi'
| 'wdtt'
| 'olcrtc'
| 'fptn'
| 'awg'
| 'warp'
| 'anytls'
@@ -362,6 +364,11 @@ export namespace Tachyon {
olcrtc_dns_server?: string;
olcrtc_subscription_links?: string[];
access_token?: string;
sni?: string;
bypass_method?: string;
preferred_server?: string;
selector_proxy_links?: string[];
subscription_urls?: string[];
interfaces?: string[];
@@ -983,6 +990,21 @@ export namespace Tachyon {
status_message: string;
}
export interface GetFptnStatus {
installed: boolean;
configured: boolean;
enabled_rule_count: number;
service_running: boolean;
pid?: number | null;
version?: string;
binary?: string;
tun_interface?: string;
route_table?: string;
log_file?: string;
ready: boolean;
status_message?: string;
}
export interface ByedpiCheckResult {
byedpi_installed: 0 | 1;
byedpi_package_installed: 0 | 1;