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;
@@ -2056,11 +2056,15 @@ function renderDefaultState({
"masque",
"openvpn"
].includes(section.action || "");
const isServiceNode = ["zapret", "zapret2", "byedpi"].includes(section.action || "") || Boolean(section.serviceStatus);
const isServiceNode = ["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 : "zapret");
const typeLabel = serviceType === "zapret" ? "Zapret" : serviceType === "zapret2" ? "Zapret2" : "ByeDPI";
const serviceType = ss?.serviceType || (["zapret", "zapret2", "byedpi", "wdtt", "olcrtc", "fptn"].includes(
section.action || ""
) ? section.action : "zapret");
const typeLabel = serviceType === "zapret" ? "Zapret" : serviceType === "zapret2" ? "Zapret2" : serviceType === "byedpi" ? "ByeDPI" : serviceType === "wdtt" ? "WDTT" : serviceType === "olcrtc" ? "OlcRTC" : "FPTN";
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" }, [
@@ -3049,6 +3053,7 @@ var Tachyon;
AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status";
AvailableMethods2["GET_WDTT_STATUS"] = "get_wdtt_status";
AvailableMethods2["GET_OLCRTC_STATUS"] = "get_olcrtc_status";
AvailableMethods2["GET_FPTN_STATUS"] = "get_fptn_status";
AvailableMethods2["CLASH_API"] = "clash_api";
AvailableMethods2["ENABLE"] = "enable";
AvailableMethods2["DISABLE"] = "disable";
@@ -3388,6 +3393,12 @@ var TachyonShellMethods = {
"/usr/bin/tachyon",
{ allowNonZeroWithStdout: true }
),
getFptnStatus: async () => callBaseMethod(
Tachyon.AvailableMethods.GET_FPTN_STATUS,
[],
"/usr/bin/tachyon",
{ allowNonZeroWithStdout: true }
),
getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES
]),
@@ -4691,7 +4702,8 @@ var ACTION_DISPLAY_NAMES = {
mieru: "Mieru",
sudoku: "Sudoku",
masque: "MASQUE",
openvpn: "OpenVPN"
openvpn: "OpenVPN",
fptn: "FPTN"
};
function isConnectionAction(action) {
return Boolean(
@@ -4700,7 +4712,9 @@ function isConnectionAction(action) {
}
function isServiceAction(action) {
return Boolean(
action && ["zapret", "zapret2", "byedpi", "wdtt", "olcrtc"].includes(action)
action && ["zapret", "zapret2", "byedpi", "wdtt", "olcrtc", "fptn"].includes(
action
)
);
}
function hasSubscriptionSources(section) {
@@ -5580,6 +5594,22 @@ async function getDashboardSections(options = {}) {
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) {
}
@@ -25,6 +25,7 @@ const ROUTING_ACTIONS = [
"byedpi",
"wdtt",
"olcrtc",
"fptn",
"awg",
"warp",
"anytls",
@@ -491,6 +492,7 @@ const actionProvidersAvailabilityState = {
byedpiInstalled: false,
wdttInstalled: false,
olcrtcInstalled: false,
fptnInstalled: false,
torInstalled: false,
singBoxExtended: false,
};
@@ -685,6 +687,12 @@ function updateActionProvidersAvailabilityState(nextState) {
);
}
if (typeof nextState.fptnInstalled !== "undefined") {
actionProvidersAvailabilityState.fptnInstalled = Boolean(
nextState.fptnInstalled,
);
}
if (typeof nextState.singBoxExtended !== "undefined") {
actionProvidersAvailabilityState.singBoxExtended = Boolean(
nextState.singBoxExtended,
@@ -711,6 +719,7 @@ function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) {
byedpiInstalled: Boolean(systemInfo.byedpi_installed),
wdttInstalled: Boolean(systemInfo.wdtt_installed),
olcrtcInstalled: Boolean(systemInfo.olcrtc_installed),
fptnInstalled: Boolean(systemInfo.fptn_installed),
singBoxExtended: Boolean(systemInfo.sing_box_extended),
torInstalled: Boolean(systemInfo.tor_installed),
});
@@ -2181,6 +2190,10 @@ function isDownloadThroughTargetSection(section, currentSectionId) {
return isOlcrtcInstalledForUi();
}
if (action === "fptn") {
return isFptnInstalledForUi();
}
return false;
}
@@ -4625,6 +4638,10 @@ function isOlcrtcInstalledForUi() {
return actionProvidersAvailabilityState.olcrtcInstalled;
}
function isFptnInstalledForUi() {
return actionProvidersAvailabilityState.fptnInstalled;
}
function isSingBoxExtendedForUi() {
return actionProvidersAvailabilityState.singBoxExtended;
}
@@ -4660,6 +4677,8 @@ function getActionOptionLabel(action) {
return "WDTT";
case "olcrtc":
return "OlcRTC";
case "fptn":
return "FPTN";
case "awg":
return "AmneziaWG";
@@ -4710,6 +4729,10 @@ function getRuleActionDisplayValue(section_id) {
return "OlcRTC";
}
if (action === "fptn") {
return "FPTN";
}
if (action === "awg") {
return "AmneziaWG";
}
@@ -4782,6 +4805,9 @@ function populateActionOptionValues(option) {
if (isOlcrtcInstalledForUi()) {
option.value("olcrtc", getActionOptionLabel("olcrtc"));
}
if (isFptnInstalledForUi()) {
option.value("fptn", getActionOptionLabel("fptn"));
}
option.value("hosts", getActionOptionLabel("hosts"));
}
@@ -7763,6 +7789,10 @@ function createSectionContent(section) {
o.load = function (section_id) {
return ensureActionProvidersAvailabilityLoaded().then(() => {
populateActionOptionValues(this);
const configured = getRuleConfiguredAction(section_id);
if (configured && !this.keylist.includes(configured)) {
this.value(configured, getActionOptionLabel(configured));
}
return this.cfgvalue(section_id);
});
};
@@ -8168,6 +8198,52 @@ function createSectionContent(section) {
o.depends("action", "olcrtc");
o.modalonly = true;
// ─── FPTN Fields ───────────────────────────────────────────────────────
o = section.taboption(
"settings",
form.Value,
"access_token",
_("FPTN Access Token"),
);
o.depends("action", "fptn");
o.password = true;
o.rmempty = false;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"sni",
_("FPTN SNI"),
_("Optional: override TLS SNI for FPTN connection"),
);
o.depends("action", "fptn");
o.optional = true;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"bypass_method",
_("FPTN Bypass Method"),
_("Optional: override bypass method (e.g. quic, tcp)"),
);
o.depends("action", "fptn");
o.optional = true;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"preferred_server",
_("FPTN Preferred Server"),
_("Optional: preferred FPTN server address"),
);
o.depends("action", "fptn");
o.optional = true;
o.modalonly = true;
// ─── Hosts Import Helpers ──────────────────────────────────────────────
function parseHostsFile(text, options) {
@@ -11310,6 +11386,7 @@ const ACTION_COLORS = {
byedpi: "#d35400",
wdtt: "#27ae60",
olcrtc: "#2980b9",
fptn: "#e74c3c",
awg: "#008080",
warp: "#e67e22",
anytls: "#16a085",
+2
View File
@@ -114,6 +114,7 @@ function show_help() {
print(" get_byedpi_status Get ByeDPI provider and Tachyon-managed ciadpi status\n");
print(" get_wdtt_status Get WDTT provider and Tachyon-managed wdtt status\n");
print(" get_olcrtc_status Get OlcRTC provider and Tachyon-managed olcrtc status\n");
print(" get_fptn_status Get FPTN provider and Tachyon-managed fptn status\n");
print(" get_system_info Get system information in JSON format\n");
print(" get_server_capabilities Get server UI capabilities in JSON format\n");
print(" get_ui_capabilities Get lightweight UI capabilities in JSON format\n");
@@ -230,6 +231,7 @@ function command_spec(command) {
get_byedpi_status: [ "diagnostics/runtime.uc", "get-byedpi-status", 0 ],
get_wdtt_status: [ "diagnostics/runtime.uc", "get-wdtt-status", 0 ],
get_olcrtc_status: [ "diagnostics/runtime.uc", "get-olcrtc-status", 0 ],
get_fptn_status: [ "diagnostics/runtime.uc", "get-fptn-status", 0 ],
get_tailscale_status: [ "diagnostics/runtime.uc", "get-tailscale-status", 0 ],
get_tailscale_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 0 ],
tailscale_restart: [ "providers/tailscale/runtime.uc", "start-runtime", 0 ],
@@ -5320,6 +5320,8 @@ else if (mode == "get-wdtt-status")
exit(module_passthrough(WDTT_RUNTIME_UC, [ "status" ]));
else if (mode == "get-olcrtc-status")
exit(module_passthrough(OLCRTC_RUNTIME_UC, [ "status" ]));
else if (mode == "get-fptn-status")
exit(module_passthrough(FPTN_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-status")
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-peers")
+5
View File
@@ -7,6 +7,7 @@ UPDATER_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updater.uc"
PACKAGES_UC="$ROOT_DIR/tachyon/files/usr/lib/core/packages.uc"
FPTN_RUNTIME_UC="$ROOT_DIR/tachyon/files/usr/lib/providers/fptn/runtime.uc"
STATE_UC="$ROOT_DIR/tachyon/files/usr/lib/service/state.uc"
DIAGNOSTICS_UC="$ROOT_DIR/tachyon/files/usr/lib/diagnostics/runtime.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
@@ -115,4 +116,8 @@ EOF_FIXTURE2
SIG_2="$(ucode_run "$STATE_UC" fptn-runtime-signature-fixture "$WORK_DIR/state_fixture2.json")"
[ "$SIG_1" != "$SIG_2" ] || fail "fptn signature should differ when token changes"
# 5. Test diagnostics runtime dispatch for get-fptn-status
STATUS_OUT="$(TACHYON_LIB_DIR="$TACHYON_LIB" ucode_run "$DIAGNOSTICS_UC" get-fptn-status)"
echo "$STATUS_OUT" | grep -q '"ready":' || fail "get-fptn-status should return status json"
echo "fptn component tests passed"