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:
+1736
-1683
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)'
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user