P9(4): remove local-core live-test trio — outbound speed test + egress probe, restart/stop xray endpoints
Per user decision 2026-10-21 (reverses the earlier keep call in PANEL-LITE-SPEC.md, now amended): the lite panel never runs a local core, so measuring/probing from the panel host is meaningless. Backend: - delete service/outbound probe implementation (probe_http.go and its tests) and the TestOutbound/TestOutbounds batch prober; outbound traffic accounting (AddTraffic/GetOutboundsTraffic/Reset) unchanged - drop POST /panel/api/xray/testOutbound + testOutbounds routes and the outboundTestUrl setting (get/update plumbing removed; stored rows in existing DBs are inert) - drop POST /panel/api/server/restartXrayService + stopXrayService routes/handlers and the node-sync allowlist entry; the service-level Stop/RestartXrayService stay for internal import/restore flows - node-sync allowlist contract test updated to the new inventory Frontend: - remove test buttons/mode switch/test-all from outbounds tab, card list and table columns; delete TestResultPopover and CountryPill; drop the egress/country/latency columns fed by probe results - remove outboundTestUrl state and save plumbing from useXraySetting and the Basics tab field; drop OutboundTestResult schemas - delete the 15 now-dead i18n keys from all 13 locales (incl. the restart/stop keys orphaned since P6/P7 — i18n-dead-keys test green) - tests: drop the test-URL hook test, re-probe the loopback index mapping through Move-to-top, regenerate API types (gen)
This commit is contained in:
@@ -3,64 +3,24 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"AllSetting": {
|
||||
"datepicker": "",
|
||||
"expireDiff": 0,
|
||||
"externalTrafficInformEnable": false,
|
||||
"externalTrafficInformURI": "",
|
||||
"ipLimitAllowlist": "",
|
||||
"ldapAutoCreate": false,
|
||||
"ldapAutoDelete": false,
|
||||
"ldapBaseDN": "",
|
||||
"ldapBindDN": "",
|
||||
"ldapDefaultExpiryDays": 0,
|
||||
"ldapDefaultLimitIP": 0,
|
||||
"ldapDefaultTotalGB": 0,
|
||||
"ldapEnable": false,
|
||||
"ldapFlagField": "",
|
||||
"ldapHost": "",
|
||||
"ldapInboundTags": "",
|
||||
"ldapInsecureSkipVerify": false,
|
||||
"ldapInvertFlag": false,
|
||||
"ldapPassword": "",
|
||||
"ldapPort": 0,
|
||||
"ldapSyncCron": "",
|
||||
"ldapTruthyValues": "",
|
||||
"ldapUseTLS": false,
|
||||
"ldapUserAttr": "",
|
||||
"ldapUserFilter": "",
|
||||
"ldapVlessField": "",
|
||||
"outboundDownThreshold": 1,
|
||||
"pageSize": 0,
|
||||
"panelOutbound": "",
|
||||
"remarkTemplate": "",
|
||||
"restartXrayOnClientDisable": false,
|
||||
"sessionMaxAge": 1,
|
||||
"smtpCpu": 0,
|
||||
"smtpEnable": false,
|
||||
"smtpEnabledEvents": "",
|
||||
"smtpEncryptionType": "",
|
||||
"smtpFrom": "",
|
||||
"smtpFromName": "",
|
||||
"smtpHost": "",
|
||||
"smtpMemory": 0,
|
||||
"smtpPassword": "",
|
||||
"smtpPort": 1,
|
||||
"smtpTo": "",
|
||||
"smtpUsername": "",
|
||||
"subAnnounce": "",
|
||||
"subCertFile": "",
|
||||
"subClashAutoDetect": false,
|
||||
"subClashEnable": false,
|
||||
"subClashEnableRouting": false,
|
||||
"subClashPath": "",
|
||||
"subClashRules": "",
|
||||
"subClashURI": "",
|
||||
"subClashUserAgentRegex": "",
|
||||
"subDomain": "",
|
||||
"subEnable": false,
|
||||
"subEnableRouting": false,
|
||||
"subEncrypt": false,
|
||||
"subHideSettings": false,
|
||||
"subIncyEnableRouting": false,
|
||||
"subIncyRoutingRules": "",
|
||||
"subJsonAlwaysArray": false,
|
||||
"subJsonAutoDetect": false,
|
||||
"subJsonEnable": false,
|
||||
@@ -68,37 +28,22 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subJsonMux": "",
|
||||
"subJsonObservatory": "",
|
||||
"subJsonPath": "",
|
||||
"subJsonRules": "",
|
||||
"subJsonURI": "",
|
||||
"subJsonUserAgentRegex": "",
|
||||
"subKeyFile": "",
|
||||
"subListen": "",
|
||||
"subPath": "",
|
||||
"subPort": 1,
|
||||
"subPort": 0,
|
||||
"subProfileUrl": "",
|
||||
"subRoutingRules": "",
|
||||
"subShowIdentityOnAllLinks": false,
|
||||
"subSupportUrl": "",
|
||||
"subThemeDir": "",
|
||||
"subTitle": "",
|
||||
"subURI": "",
|
||||
"subUpdates": 0,
|
||||
"tgBotAPIServer": "",
|
||||
"tgBotBackup": false,
|
||||
"tgBotChatId": "",
|
||||
"tgBotEnable": false,
|
||||
"tgBotProxy": "",
|
||||
"tgBotToken": "",
|
||||
"tgCpu": 0,
|
||||
"tgEnabledEvents": "",
|
||||
"tgLang": "",
|
||||
"tgMemory": 0,
|
||||
"tgRunTime": "",
|
||||
"timeLocation": "",
|
||||
"trafficDiff": 0,
|
||||
"trustedProxyCIDRs": "",
|
||||
"twoFactorEnable": false,
|
||||
"twoFactorToken": "",
|
||||
"warpUpdateInterval": 0,
|
||||
"webBasePath": "",
|
||||
"webCertFile": "",
|
||||
@@ -110,71 +55,27 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"AllSettingView": {
|
||||
"datepicker": "",
|
||||
"expireDiff": 0,
|
||||
"externalTrafficInformEnable": false,
|
||||
"externalTrafficInformURI": "",
|
||||
"hasApiToken": false,
|
||||
"hasLdapPassword": false,
|
||||
"hasNordSecret": false,
|
||||
"hasSmtpPassword": false,
|
||||
"hasTgBotToken": false,
|
||||
"hasTwoFactorToken": false,
|
||||
"hasWarpSecret": false,
|
||||
"ipLimitAllowlist": "",
|
||||
"ldapAutoCreate": false,
|
||||
"ldapAutoDelete": false,
|
||||
"ldapBaseDN": "",
|
||||
"ldapBindDN": "",
|
||||
"ldapDefaultExpiryDays": 0,
|
||||
"ldapDefaultLimitIP": 0,
|
||||
"ldapDefaultTotalGB": 0,
|
||||
"ldapEnable": false,
|
||||
"ldapFlagField": "",
|
||||
"ldapHost": "",
|
||||
"ldapInboundTags": "",
|
||||
"ldapInsecureSkipVerify": false,
|
||||
"ldapInvertFlag": false,
|
||||
"ldapPassword": "",
|
||||
"ldapPort": 0,
|
||||
"ldapSyncCron": "",
|
||||
"ldapTruthyValues": "",
|
||||
"ldapUseTLS": false,
|
||||
"ldapUserAttr": "",
|
||||
"ldapUserFilter": "",
|
||||
"ldapVlessField": "",
|
||||
"outboundDownThreshold": 1,
|
||||
"pageSize": 0,
|
||||
"panelOutbound": "",
|
||||
"remarkTemplate": "",
|
||||
"restartXrayOnClientDisable": false,
|
||||
"sessionMaxAge": 1,
|
||||
"smtpCpu": 0,
|
||||
"smtpEnable": false,
|
||||
"smtpEnabledEvents": "",
|
||||
"smtpEncryptionType": "",
|
||||
"smtpFrom": "",
|
||||
"smtpFromName": "",
|
||||
"smtpHost": "",
|
||||
"smtpMemory": 0,
|
||||
"smtpPassword": "",
|
||||
"smtpPort": 1,
|
||||
"smtpTo": "",
|
||||
"smtpUsername": "",
|
||||
"subAnnounce": "",
|
||||
"subCertFile": "",
|
||||
"subClashAutoDetect": false,
|
||||
"subClashEnable": false,
|
||||
"subClashEnableRouting": false,
|
||||
"subClashPath": "",
|
||||
"subClashRules": "",
|
||||
"subClashURI": "",
|
||||
"subClashUserAgentRegex": "",
|
||||
"subDomain": "",
|
||||
"subEnable": false,
|
||||
"subEnableRouting": false,
|
||||
"subEncrypt": false,
|
||||
"subHideSettings": false,
|
||||
"subIncyEnableRouting": false,
|
||||
"subIncyRoutingRules": "",
|
||||
"subJsonAlwaysArray": false,
|
||||
"subJsonAutoDetect": false,
|
||||
"subJsonEnable": false,
|
||||
@@ -182,37 +83,22 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subJsonMux": "",
|
||||
"subJsonObservatory": "",
|
||||
"subJsonPath": "",
|
||||
"subJsonRules": "",
|
||||
"subJsonURI": "",
|
||||
"subJsonUserAgentRegex": "",
|
||||
"subKeyFile": "",
|
||||
"subListen": "",
|
||||
"subPath": "",
|
||||
"subPort": 1,
|
||||
"subPort": 0,
|
||||
"subProfileUrl": "",
|
||||
"subRoutingRules": "",
|
||||
"subShowIdentityOnAllLinks": false,
|
||||
"subSupportUrl": "",
|
||||
"subThemeDir": "",
|
||||
"subTitle": "",
|
||||
"subURI": "",
|
||||
"subUpdates": 0,
|
||||
"tgBotAPIServer": "",
|
||||
"tgBotBackup": false,
|
||||
"tgBotChatId": "",
|
||||
"tgBotEnable": false,
|
||||
"tgBotProxy": "",
|
||||
"tgBotToken": "",
|
||||
"tgCpu": 0,
|
||||
"tgEnabledEvents": "",
|
||||
"tgLang": "",
|
||||
"tgMemory": 0,
|
||||
"tgRunTime": "",
|
||||
"timeLocation": "",
|
||||
"trafficDiff": 0,
|
||||
"trustedProxyCIDRs": "",
|
||||
"twoFactorEnable": false,
|
||||
"twoFactorToken": "",
|
||||
"warpUpdateInterval": 0,
|
||||
"webBasePath": "",
|
||||
"webCertFile": "",
|
||||
@@ -221,26 +107,6 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"webListen": "",
|
||||
"webPort": 1
|
||||
},
|
||||
"AmneziaWGLogs": {
|
||||
"events": [
|
||||
"2025/01/01 12:00:00 amneziawg: started interface awg1 for inbound 1"
|
||||
],
|
||||
"peers": [
|
||||
{
|
||||
"allowedIPs": "10.8.1.2/32",
|
||||
"down": 4194304,
|
||||
"email": "peer@example.com",
|
||||
"endpoint": "203.0.113.9:51820",
|
||||
"handshake": 1735732800000,
|
||||
"inboundId": 1,
|
||||
"interface": "awg1",
|
||||
"online": true,
|
||||
"tag": "inbound-51820",
|
||||
"up": 1048576
|
||||
}
|
||||
],
|
||||
"running": true
|
||||
},
|
||||
"ApiToken": {
|
||||
"createdAt": 0,
|
||||
"enabled": false,
|
||||
@@ -648,9 +514,10 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
""
|
||||
],
|
||||
"name": "",
|
||||
"nodeType": "traditional",
|
||||
"outboundTag": "",
|
||||
"pinnedCertSha256": "",
|
||||
"port": 1,
|
||||
"port": 0,
|
||||
"remark": "",
|
||||
"scheme": "http",
|
||||
"tlsVerifyMode": "verify"
|
||||
@@ -683,6 +550,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"name": "edge-1",
|
||||
"netDown": 1048576,
|
||||
"netUp": 2097152,
|
||||
"nodeType": "traditional",
|
||||
"onlineCount": 5,
|
||||
"outboundTag": "direct",
|
||||
"panelVersion": "v3.x.x",
|
||||
@@ -713,18 +581,6 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"runId": "1735689600123456789",
|
||||
"state": "success"
|
||||
},
|
||||
"PeerActivity": {
|
||||
"allowedIPs": "10.8.1.2/32",
|
||||
"down": 4194304,
|
||||
"email": "peer@example.com",
|
||||
"endpoint": "203.0.113.9:51820",
|
||||
"handshake": 1735732800000,
|
||||
"inboundId": 1,
|
||||
"interface": "awg1",
|
||||
"online": true,
|
||||
"tag": "inbound-51820",
|
||||
"up": 1048576
|
||||
},
|
||||
"ProbeResultUI": {
|
||||
"cpuPct": 12.5,
|
||||
"error": "",
|
||||
|
||||
@@ -9,83 +9,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"externalTrafficInformEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"externalTrafficInformURI": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipLimitAllowlist": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapAutoCreate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapAutoDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapBaseDN": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapBindDN": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapDefaultExpiryDays": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapDefaultLimitIP": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapDefaultTotalGB": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapFlagField": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapInboundTags": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapInsecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapInvertFlag": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapPassword": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapPort": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapSyncCron": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapTruthyValues": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapUseTLS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapUserAttr": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapUserFilter": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapVlessField": {
|
||||
"type": "string"
|
||||
},
|
||||
"outboundDownThreshold": {
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
@@ -110,48 +36,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpCpu": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"smtpEnabledEvents": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpEncryptionType": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpFromName": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpMemory": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpPassword": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpPort": {
|
||||
"maximum": 65535,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpUsername": {
|
||||
"type": "string"
|
||||
},
|
||||
"subAnnounce": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -164,15 +48,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashURI": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -185,21 +63,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subEncrypt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subHideSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subIncyEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subIncyRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonAlwaysArray": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -221,9 +90,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subJsonPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonURI": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -240,16 +106,14 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "string"
|
||||
},
|
||||
"subPort": {
|
||||
"description": "SubPort 0 means \"no explicit port\": subscription links are generated\nwithout a port (standard scheme port behind a reverse proxy) while the\nsub server still binds the default port. See SettingService.BuildSubURIBase\nand the sub server bootstrap for the two sides of that split.",
|
||||
"maximum": 65535,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"subProfileUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"subRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subShowIdentityOnAllLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -270,43 +134,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgBotAPIServer": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotBackup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tgBotChatId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tgBotProxy": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgCpu": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgEnabledEvents": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgLang": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgMemory": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgRunTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeLocation": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -318,12 +145,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"trustedProxyCIDRs": {
|
||||
"type": "string"
|
||||
},
|
||||
"twoFactorEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"twoFactorToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"warpUpdateInterval": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
@@ -352,64 +173,24 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"required": [
|
||||
"datepicker",
|
||||
"expireDiff",
|
||||
"externalTrafficInformEnable",
|
||||
"externalTrafficInformURI",
|
||||
"ipLimitAllowlist",
|
||||
"ldapAutoCreate",
|
||||
"ldapAutoDelete",
|
||||
"ldapBaseDN",
|
||||
"ldapBindDN",
|
||||
"ldapDefaultExpiryDays",
|
||||
"ldapDefaultLimitIP",
|
||||
"ldapDefaultTotalGB",
|
||||
"ldapEnable",
|
||||
"ldapFlagField",
|
||||
"ldapHost",
|
||||
"ldapInboundTags",
|
||||
"ldapInsecureSkipVerify",
|
||||
"ldapInvertFlag",
|
||||
"ldapPassword",
|
||||
"ldapPort",
|
||||
"ldapSyncCron",
|
||||
"ldapTruthyValues",
|
||||
"ldapUseTLS",
|
||||
"ldapUserAttr",
|
||||
"ldapUserFilter",
|
||||
"ldapVlessField",
|
||||
"outboundDownThreshold",
|
||||
"pageSize",
|
||||
"panelOutbound",
|
||||
"remarkTemplate",
|
||||
"restartXrayOnClientDisable",
|
||||
"sessionMaxAge",
|
||||
"smtpCpu",
|
||||
"smtpEnable",
|
||||
"smtpEnabledEvents",
|
||||
"smtpEncryptionType",
|
||||
"smtpFrom",
|
||||
"smtpFromName",
|
||||
"smtpHost",
|
||||
"smtpMemory",
|
||||
"smtpPassword",
|
||||
"smtpPort",
|
||||
"smtpTo",
|
||||
"smtpUsername",
|
||||
"subAnnounce",
|
||||
"subCertFile",
|
||||
"subClashAutoDetect",
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
"subDomain",
|
||||
"subEnable",
|
||||
"subEnableRouting",
|
||||
"subEncrypt",
|
||||
"subHideSettings",
|
||||
"subIncyEnableRouting",
|
||||
"subIncyRoutingRules",
|
||||
"subJsonAlwaysArray",
|
||||
"subJsonAutoDetect",
|
||||
"subJsonEnable",
|
||||
@@ -417,7 +198,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subJsonMux",
|
||||
"subJsonObservatory",
|
||||
"subJsonPath",
|
||||
"subJsonRules",
|
||||
"subJsonURI",
|
||||
"subJsonUserAgentRegex",
|
||||
"subKeyFile",
|
||||
@@ -425,29 +205,15 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subPath",
|
||||
"subPort",
|
||||
"subProfileUrl",
|
||||
"subRoutingRules",
|
||||
"subShowIdentityOnAllLinks",
|
||||
"subSupportUrl",
|
||||
"subThemeDir",
|
||||
"subTitle",
|
||||
"subURI",
|
||||
"subUpdates",
|
||||
"tgBotAPIServer",
|
||||
"tgBotBackup",
|
||||
"tgBotChatId",
|
||||
"tgBotEnable",
|
||||
"tgBotProxy",
|
||||
"tgBotToken",
|
||||
"tgCpu",
|
||||
"tgEnabledEvents",
|
||||
"tgLang",
|
||||
"tgMemory",
|
||||
"tgRunTime",
|
||||
"timeLocation",
|
||||
"trafficDiff",
|
||||
"trustedProxyCIDRs",
|
||||
"twoFactorEnable",
|
||||
"twoFactorToken",
|
||||
"warpUpdateInterval",
|
||||
"webBasePath",
|
||||
"webCertFile",
|
||||
@@ -467,104 +233,18 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"externalTrafficInformEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"externalTrafficInformURI": {
|
||||
"type": "string"
|
||||
},
|
||||
"hasApiToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasLdapPassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasNordSecret": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasSmtpPassword": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasTgBotToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasTwoFactorToken": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasWarpSecret": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipLimitAllowlist": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapAutoCreate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapAutoDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapBaseDN": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapBindDN": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapDefaultExpiryDays": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapDefaultLimitIP": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapDefaultTotalGB": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapFlagField": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapInboundTags": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapInsecureSkipVerify": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapInvertFlag": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapPassword": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapPort": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"ldapSyncCron": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapTruthyValues": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapUseTLS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ldapUserAttr": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapUserFilter": {
|
||||
"type": "string"
|
||||
},
|
||||
"ldapVlessField": {
|
||||
"type": "string"
|
||||
},
|
||||
"outboundDownThreshold": {
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
@@ -589,48 +269,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpCpu": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"smtpEnabledEvents": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpEncryptionType": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpFrom": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpFromName": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpHost": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpMemory": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpPassword": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpPort": {
|
||||
"maximum": 65535,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"smtpTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"smtpUsername": {
|
||||
"type": "string"
|
||||
},
|
||||
"subAnnounce": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -643,15 +281,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashURI": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -664,21 +296,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subEncrypt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subHideSettings": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subIncyEnableRouting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subIncyRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonAlwaysArray": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -700,9 +323,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subJsonPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonURI": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -719,16 +339,14 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "string"
|
||||
},
|
||||
"subPort": {
|
||||
"description": "SubPort 0 means \"no explicit port\": subscription links are generated\nwithout a port (standard scheme port behind a reverse proxy) while the\nsub server still binds the default port. See SettingService.BuildSubURIBase\nand the sub server bootstrap for the two sides of that split.",
|
||||
"maximum": 65535,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"subProfileUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"subRoutingRules": {
|
||||
"type": "string"
|
||||
},
|
||||
"subShowIdentityOnAllLinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -749,43 +367,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgBotAPIServer": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotBackup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tgBotChatId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tgBotProxy": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgBotToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgCpu": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgEnabledEvents": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgLang": {
|
||||
"type": "string"
|
||||
},
|
||||
"tgMemory": {
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tgRunTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeLocation": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -797,12 +378,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"trustedProxyCIDRs": {
|
||||
"type": "string"
|
||||
},
|
||||
"twoFactorEnable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"twoFactorToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"warpUpdateInterval": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
@@ -831,71 +406,27 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"required": [
|
||||
"datepicker",
|
||||
"expireDiff",
|
||||
"externalTrafficInformEnable",
|
||||
"externalTrafficInformURI",
|
||||
"hasApiToken",
|
||||
"hasLdapPassword",
|
||||
"hasNordSecret",
|
||||
"hasSmtpPassword",
|
||||
"hasTgBotToken",
|
||||
"hasTwoFactorToken",
|
||||
"hasWarpSecret",
|
||||
"ipLimitAllowlist",
|
||||
"ldapAutoCreate",
|
||||
"ldapAutoDelete",
|
||||
"ldapBaseDN",
|
||||
"ldapBindDN",
|
||||
"ldapDefaultExpiryDays",
|
||||
"ldapDefaultLimitIP",
|
||||
"ldapDefaultTotalGB",
|
||||
"ldapEnable",
|
||||
"ldapFlagField",
|
||||
"ldapHost",
|
||||
"ldapInboundTags",
|
||||
"ldapInsecureSkipVerify",
|
||||
"ldapInvertFlag",
|
||||
"ldapPassword",
|
||||
"ldapPort",
|
||||
"ldapSyncCron",
|
||||
"ldapTruthyValues",
|
||||
"ldapUseTLS",
|
||||
"ldapUserAttr",
|
||||
"ldapUserFilter",
|
||||
"ldapVlessField",
|
||||
"outboundDownThreshold",
|
||||
"pageSize",
|
||||
"panelOutbound",
|
||||
"remarkTemplate",
|
||||
"restartXrayOnClientDisable",
|
||||
"sessionMaxAge",
|
||||
"smtpCpu",
|
||||
"smtpEnable",
|
||||
"smtpEnabledEvents",
|
||||
"smtpEncryptionType",
|
||||
"smtpFrom",
|
||||
"smtpFromName",
|
||||
"smtpHost",
|
||||
"smtpMemory",
|
||||
"smtpPassword",
|
||||
"smtpPort",
|
||||
"smtpTo",
|
||||
"smtpUsername",
|
||||
"subAnnounce",
|
||||
"subCertFile",
|
||||
"subClashAutoDetect",
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
"subDomain",
|
||||
"subEnable",
|
||||
"subEnableRouting",
|
||||
"subEncrypt",
|
||||
"subHideSettings",
|
||||
"subIncyEnableRouting",
|
||||
"subIncyRoutingRules",
|
||||
"subJsonAlwaysArray",
|
||||
"subJsonAutoDetect",
|
||||
"subJsonEnable",
|
||||
@@ -903,7 +434,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subJsonMux",
|
||||
"subJsonObservatory",
|
||||
"subJsonPath",
|
||||
"subJsonRules",
|
||||
"subJsonURI",
|
||||
"subJsonUserAgentRegex",
|
||||
"subKeyFile",
|
||||
@@ -911,29 +441,15 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subPath",
|
||||
"subPort",
|
||||
"subProfileUrl",
|
||||
"subRoutingRules",
|
||||
"subShowIdentityOnAllLinks",
|
||||
"subSupportUrl",
|
||||
"subThemeDir",
|
||||
"subTitle",
|
||||
"subURI",
|
||||
"subUpdates",
|
||||
"tgBotAPIServer",
|
||||
"tgBotBackup",
|
||||
"tgBotChatId",
|
||||
"tgBotEnable",
|
||||
"tgBotProxy",
|
||||
"tgBotToken",
|
||||
"tgCpu",
|
||||
"tgEnabledEvents",
|
||||
"tgLang",
|
||||
"tgMemory",
|
||||
"tgRunTime",
|
||||
"timeLocation",
|
||||
"trafficDiff",
|
||||
"trustedProxyCIDRs",
|
||||
"twoFactorEnable",
|
||||
"twoFactorToken",
|
||||
"warpUpdateInterval",
|
||||
"webBasePath",
|
||||
"webCertFile",
|
||||
@@ -944,36 +460,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AmneziaWGLogs": {
|
||||
"description": "AmneziaWGLogs is what the overview's AmneziaWG log view renders: the live\nper-peer activity of every running embedded interface, plus the panel's\nown recent AmneziaWG lifecycle log lines that explain a peer being absent\nfrom Peers at all.",
|
||||
"properties": {
|
||||
"events": {
|
||||
"example": [
|
||||
"2025/01/01 12:00:00 amneziawg: started interface awg1 for inbound 1"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"peers": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PeerActivity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"running": {
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"events",
|
||||
"peers",
|
||||
"running"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ApiToken": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@@ -2617,6 +2103,14 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"nodeType": {
|
||||
"description": "NodeType distinguishes traditional panel-to-panel nodes from lightweight\nagent nodes. Agent nodes skip the panel reachability probe and the API\ntoken requirement; an agent token is issued on creation instead.",
|
||||
"enum": [
|
||||
"traditional",
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"outboundTag": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2625,7 +2119,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"port": {
|
||||
"maximum": 65535,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
@@ -2778,6 +2272,11 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"nodeType": {
|
||||
"description": "NodeType is \"agent\" for lightweight agent-managed nodes (an agent is\nbound to the node) and \"traditional\" otherwise. It is derived from the\nagent binding, not stored on the node row; read paths that serve the\npanel UI annotate it via annotateNodeTypes. Empty on views built\noutside those paths.",
|
||||
"example": "traditional",
|
||||
"type": "string"
|
||||
},
|
||||
"onlineCount": {
|
||||
"example": 5,
|
||||
"type": "integer"
|
||||
@@ -2945,68 +2444,6 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PeerActivity": {
|
||||
"description": "PeerActivity is one peer's live embedded-Device-reported state, the\ncounterpart of an Xray access-log entry: a tunnel logs no requests, only\nhandshakes and bytes.",
|
||||
"properties": {
|
||||
"allowedIPs": {
|
||||
"example": "10.8.1.2/32",
|
||||
"type": "string"
|
||||
},
|
||||
"down": {
|
||||
"example": 4194304,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
"example": "peer@example.com",
|
||||
"type": "string"
|
||||
},
|
||||
"endpoint": {
|
||||
"example": "203.0.113.9:51820",
|
||||
"type": "string"
|
||||
},
|
||||
"handshake": {
|
||||
"description": "Handshake is unix milliseconds, 0 when the peer has never connected.",
|
||||
"example": 1735732800000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"inboundId": {
|
||||
"example": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"interface": {
|
||||
"example": "awg1",
|
||||
"type": "string"
|
||||
},
|
||||
"online": {
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"tag": {
|
||||
"example": "inbound-51820",
|
||||
"type": "string"
|
||||
},
|
||||
"up": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allowedIPs",
|
||||
"down",
|
||||
"email",
|
||||
"endpoint",
|
||||
"handshake",
|
||||
"inboundId",
|
||||
"interface",
|
||||
"online",
|
||||
"tag",
|
||||
"up"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProbeResultUI": {
|
||||
"properties": {
|
||||
"cpuPct": {
|
||||
|
||||
@@ -4,6 +4,7 @@ export type OnlineAPISupport = number;
|
||||
export type ProcessState = string;
|
||||
export type Protocol = string;
|
||||
export type SubLinkProvider = unknown;
|
||||
export type rawBytes = number[];
|
||||
export type staticEgressResolver = string;
|
||||
export type trafficLocalApplyAction = number;
|
||||
export type transportBits = number;
|
||||
@@ -11,64 +12,24 @@ export type transportBits = number;
|
||||
export interface AllSetting {
|
||||
datepicker: string;
|
||||
expireDiff: number;
|
||||
externalTrafficInformEnable: boolean;
|
||||
externalTrafficInformURI: string;
|
||||
ipLimitAllowlist: string;
|
||||
ldapAutoCreate: boolean;
|
||||
ldapAutoDelete: boolean;
|
||||
ldapBaseDN: string;
|
||||
ldapBindDN: string;
|
||||
ldapDefaultExpiryDays: number;
|
||||
ldapDefaultLimitIP: number;
|
||||
ldapDefaultTotalGB: number;
|
||||
ldapEnable: boolean;
|
||||
ldapFlagField: string;
|
||||
ldapHost: string;
|
||||
ldapInboundTags: string;
|
||||
ldapInsecureSkipVerify: boolean;
|
||||
ldapInvertFlag: boolean;
|
||||
ldapPassword: string;
|
||||
ldapPort: number;
|
||||
ldapSyncCron: string;
|
||||
ldapTruthyValues: string;
|
||||
ldapUseTLS: boolean;
|
||||
ldapUserAttr: string;
|
||||
ldapUserFilter: string;
|
||||
ldapVlessField: string;
|
||||
outboundDownThreshold: number;
|
||||
pageSize: number;
|
||||
panelOutbound: string;
|
||||
remarkTemplate: string;
|
||||
restartXrayOnClientDisable: boolean;
|
||||
sessionMaxAge: number;
|
||||
smtpCpu: number;
|
||||
smtpEnable: boolean;
|
||||
smtpEnabledEvents: string;
|
||||
smtpEncryptionType: string;
|
||||
smtpFrom: string;
|
||||
smtpFromName: string;
|
||||
smtpHost: string;
|
||||
smtpMemory: number;
|
||||
smtpPassword: string;
|
||||
smtpPort: number;
|
||||
smtpTo: string;
|
||||
smtpUsername: string;
|
||||
subAnnounce: string;
|
||||
subCertFile: string;
|
||||
subClashAutoDetect: boolean;
|
||||
subClashEnable: boolean;
|
||||
subClashEnableRouting: boolean;
|
||||
subClashPath: string;
|
||||
subClashRules: string;
|
||||
subClashURI: string;
|
||||
subClashUserAgentRegex: string;
|
||||
subDomain: string;
|
||||
subEnable: boolean;
|
||||
subEnableRouting: boolean;
|
||||
subEncrypt: boolean;
|
||||
subHideSettings: boolean;
|
||||
subIncyEnableRouting: boolean;
|
||||
subIncyRoutingRules: string;
|
||||
subJsonAlwaysArray: boolean;
|
||||
subJsonAutoDetect: boolean;
|
||||
subJsonEnable: boolean;
|
||||
@@ -76,7 +37,6 @@ export interface AllSetting {
|
||||
subJsonMux: string;
|
||||
subJsonObservatory: string;
|
||||
subJsonPath: string;
|
||||
subJsonRules: string;
|
||||
subJsonURI: string;
|
||||
subJsonUserAgentRegex: string;
|
||||
subKeyFile: string;
|
||||
@@ -84,29 +44,15 @@ export interface AllSetting {
|
||||
subPath: string;
|
||||
subPort: number;
|
||||
subProfileUrl: string;
|
||||
subRoutingRules: string;
|
||||
subShowIdentityOnAllLinks: boolean;
|
||||
subSupportUrl: string;
|
||||
subThemeDir: string;
|
||||
subTitle: string;
|
||||
subURI: string;
|
||||
subUpdates: number;
|
||||
tgBotAPIServer: string;
|
||||
tgBotBackup: boolean;
|
||||
tgBotChatId: string;
|
||||
tgBotEnable: boolean;
|
||||
tgBotProxy: string;
|
||||
tgBotToken: string;
|
||||
tgCpu: number;
|
||||
tgEnabledEvents: string;
|
||||
tgLang: string;
|
||||
tgMemory: number;
|
||||
tgRunTime: string;
|
||||
timeLocation: string;
|
||||
trafficDiff: number;
|
||||
trustedProxyCIDRs: string;
|
||||
twoFactorEnable: boolean;
|
||||
twoFactorToken: string;
|
||||
warpUpdateInterval: number;
|
||||
webBasePath: string;
|
||||
webCertFile: string;
|
||||
@@ -119,71 +65,27 @@ export interface AllSetting {
|
||||
export interface AllSettingView {
|
||||
datepicker: string;
|
||||
expireDiff: number;
|
||||
externalTrafficInformEnable: boolean;
|
||||
externalTrafficInformURI: string;
|
||||
hasApiToken: boolean;
|
||||
hasLdapPassword: boolean;
|
||||
hasNordSecret: boolean;
|
||||
hasSmtpPassword: boolean;
|
||||
hasTgBotToken: boolean;
|
||||
hasTwoFactorToken: boolean;
|
||||
hasWarpSecret: boolean;
|
||||
ipLimitAllowlist: string;
|
||||
ldapAutoCreate: boolean;
|
||||
ldapAutoDelete: boolean;
|
||||
ldapBaseDN: string;
|
||||
ldapBindDN: string;
|
||||
ldapDefaultExpiryDays: number;
|
||||
ldapDefaultLimitIP: number;
|
||||
ldapDefaultTotalGB: number;
|
||||
ldapEnable: boolean;
|
||||
ldapFlagField: string;
|
||||
ldapHost: string;
|
||||
ldapInboundTags: string;
|
||||
ldapInsecureSkipVerify: boolean;
|
||||
ldapInvertFlag: boolean;
|
||||
ldapPassword: string;
|
||||
ldapPort: number;
|
||||
ldapSyncCron: string;
|
||||
ldapTruthyValues: string;
|
||||
ldapUseTLS: boolean;
|
||||
ldapUserAttr: string;
|
||||
ldapUserFilter: string;
|
||||
ldapVlessField: string;
|
||||
outboundDownThreshold: number;
|
||||
pageSize: number;
|
||||
panelOutbound: string;
|
||||
remarkTemplate: string;
|
||||
restartXrayOnClientDisable: boolean;
|
||||
sessionMaxAge: number;
|
||||
smtpCpu: number;
|
||||
smtpEnable: boolean;
|
||||
smtpEnabledEvents: string;
|
||||
smtpEncryptionType: string;
|
||||
smtpFrom: string;
|
||||
smtpFromName: string;
|
||||
smtpHost: string;
|
||||
smtpMemory: number;
|
||||
smtpPassword: string;
|
||||
smtpPort: number;
|
||||
smtpTo: string;
|
||||
smtpUsername: string;
|
||||
subAnnounce: string;
|
||||
subCertFile: string;
|
||||
subClashAutoDetect: boolean;
|
||||
subClashEnable: boolean;
|
||||
subClashEnableRouting: boolean;
|
||||
subClashPath: string;
|
||||
subClashRules: string;
|
||||
subClashURI: string;
|
||||
subClashUserAgentRegex: string;
|
||||
subDomain: string;
|
||||
subEnable: boolean;
|
||||
subEnableRouting: boolean;
|
||||
subEncrypt: boolean;
|
||||
subHideSettings: boolean;
|
||||
subIncyEnableRouting: boolean;
|
||||
subIncyRoutingRules: string;
|
||||
subJsonAlwaysArray: boolean;
|
||||
subJsonAutoDetect: boolean;
|
||||
subJsonEnable: boolean;
|
||||
@@ -191,7 +93,6 @@ export interface AllSettingView {
|
||||
subJsonMux: string;
|
||||
subJsonObservatory: string;
|
||||
subJsonPath: string;
|
||||
subJsonRules: string;
|
||||
subJsonURI: string;
|
||||
subJsonUserAgentRegex: string;
|
||||
subKeyFile: string;
|
||||
@@ -199,29 +100,15 @@ export interface AllSettingView {
|
||||
subPath: string;
|
||||
subPort: number;
|
||||
subProfileUrl: string;
|
||||
subRoutingRules: string;
|
||||
subShowIdentityOnAllLinks: boolean;
|
||||
subSupportUrl: string;
|
||||
subThemeDir: string;
|
||||
subTitle: string;
|
||||
subURI: string;
|
||||
subUpdates: number;
|
||||
tgBotAPIServer: string;
|
||||
tgBotBackup: boolean;
|
||||
tgBotChatId: string;
|
||||
tgBotEnable: boolean;
|
||||
tgBotProxy: string;
|
||||
tgBotToken: string;
|
||||
tgCpu: number;
|
||||
tgEnabledEvents: string;
|
||||
tgLang: string;
|
||||
tgMemory: number;
|
||||
tgRunTime: string;
|
||||
timeLocation: string;
|
||||
trafficDiff: number;
|
||||
trustedProxyCIDRs: string;
|
||||
twoFactorEnable: boolean;
|
||||
twoFactorToken: string;
|
||||
warpUpdateInterval: number;
|
||||
webBasePath: string;
|
||||
webCertFile: string;
|
||||
@@ -231,12 +118,6 @@ export interface AllSettingView {
|
||||
webPort: number;
|
||||
}
|
||||
|
||||
export interface AmneziaWGLogs {
|
||||
events: string[];
|
||||
peers: PeerActivity[];
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface ApiToken {
|
||||
createdAt: number;
|
||||
enabled: boolean;
|
||||
@@ -601,6 +482,7 @@ export interface NodeMutationRequest {
|
||||
inboundSyncMode: string;
|
||||
inboundTags: string[];
|
||||
name: string;
|
||||
nodeType?: string;
|
||||
outboundTag: string;
|
||||
pinnedCertSha256: string;
|
||||
port: number;
|
||||
@@ -635,6 +517,7 @@ export interface NodeView {
|
||||
name: string;
|
||||
netDown: number;
|
||||
netUp: number;
|
||||
nodeType?: string;
|
||||
onlineCount: number;
|
||||
outboundTag: string;
|
||||
panelVersion: string;
|
||||
@@ -668,19 +551,6 @@ export interface PanelUpdateStatus {
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface PeerActivity {
|
||||
allowedIPs: string;
|
||||
down: number;
|
||||
email: string;
|
||||
endpoint: string;
|
||||
handshake: number;
|
||||
inboundId: number;
|
||||
interface: string;
|
||||
online: boolean;
|
||||
tag: string;
|
||||
up: number;
|
||||
}
|
||||
|
||||
export interface ProbeResultUI {
|
||||
cpuPct: number;
|
||||
error: string;
|
||||
|
||||
@@ -15,6 +15,9 @@ export type Protocol = z.infer<typeof ProtocolSchema>;
|
||||
export const SubLinkProviderSchema = z.unknown();
|
||||
export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
||||
|
||||
export const rawBytesSchema = z.array(z.number().int());
|
||||
export type rawBytes = z.infer<typeof rawBytesSchema>;
|
||||
|
||||
export const staticEgressResolverSchema = z.string();
|
||||
export type staticEgressResolver = z.infer<typeof staticEgressResolverSchema>;
|
||||
|
||||
@@ -27,64 +30,24 @@ export type transportBits = z.infer<typeof transportBitsSchema>;
|
||||
export const AllSettingSchema = z.object({
|
||||
datepicker: z.string(),
|
||||
expireDiff: z.number().int().min(0),
|
||||
externalTrafficInformEnable: z.boolean(),
|
||||
externalTrafficInformURI: z.string(),
|
||||
ipLimitAllowlist: z.string(),
|
||||
ldapAutoCreate: z.boolean(),
|
||||
ldapAutoDelete: z.boolean(),
|
||||
ldapBaseDN: z.string(),
|
||||
ldapBindDN: z.string(),
|
||||
ldapDefaultExpiryDays: z.number().int().min(0),
|
||||
ldapDefaultLimitIP: z.number().int().min(0),
|
||||
ldapDefaultTotalGB: z.number().int().min(0),
|
||||
ldapEnable: z.boolean(),
|
||||
ldapFlagField: z.string(),
|
||||
ldapHost: z.string(),
|
||||
ldapInboundTags: z.string(),
|
||||
ldapInsecureSkipVerify: z.boolean(),
|
||||
ldapInvertFlag: z.boolean(),
|
||||
ldapPassword: z.string(),
|
||||
ldapPort: z.number().int().min(0).max(65535),
|
||||
ldapSyncCron: z.string(),
|
||||
ldapTruthyValues: z.string(),
|
||||
ldapUseTLS: z.boolean(),
|
||||
ldapUserAttr: z.string(),
|
||||
ldapUserFilter: z.string(),
|
||||
ldapVlessField: z.string(),
|
||||
outboundDownThreshold: z.number().int().min(1).max(100),
|
||||
pageSize: z.number().int().min(0).max(1000),
|
||||
panelOutbound: z.string(),
|
||||
remarkTemplate: z.string(),
|
||||
restartXrayOnClientDisable: z.boolean(),
|
||||
sessionMaxAge: z.number().int().min(1).max(525600),
|
||||
smtpCpu: z.number().int().min(0).max(100),
|
||||
smtpEnable: z.boolean(),
|
||||
smtpEnabledEvents: z.string(),
|
||||
smtpEncryptionType: z.string(),
|
||||
smtpFrom: z.string(),
|
||||
smtpFromName: z.string(),
|
||||
smtpHost: z.string(),
|
||||
smtpMemory: z.number().int().min(0).max(100),
|
||||
smtpPassword: z.string(),
|
||||
smtpPort: z.number().int().min(1).max(65535),
|
||||
smtpTo: z.string(),
|
||||
smtpUsername: z.string(),
|
||||
subAnnounce: z.string(),
|
||||
subCertFile: z.string(),
|
||||
subClashAutoDetect: z.boolean(),
|
||||
subClashEnable: z.boolean(),
|
||||
subClashEnableRouting: z.boolean(),
|
||||
subClashPath: z.string(),
|
||||
subClashRules: z.string(),
|
||||
subClashURI: z.string(),
|
||||
subClashUserAgentRegex: z.string(),
|
||||
subDomain: z.string(),
|
||||
subEnable: z.boolean(),
|
||||
subEnableRouting: z.boolean(),
|
||||
subEncrypt: z.boolean(),
|
||||
subHideSettings: z.boolean(),
|
||||
subIncyEnableRouting: z.boolean(),
|
||||
subIncyRoutingRules: z.string(),
|
||||
subJsonAlwaysArray: z.boolean(),
|
||||
subJsonAutoDetect: z.boolean(),
|
||||
subJsonEnable: z.boolean(),
|
||||
@@ -92,7 +55,6 @@ export const AllSettingSchema = z.object({
|
||||
subJsonMux: z.string(),
|
||||
subJsonObservatory: z.string(),
|
||||
subJsonPath: z.string(),
|
||||
subJsonRules: z.string(),
|
||||
subJsonURI: z.string(),
|
||||
subJsonUserAgentRegex: z.string(),
|
||||
subKeyFile: z.string(),
|
||||
@@ -100,29 +62,15 @@ export const AllSettingSchema = z.object({
|
||||
subPath: z.string(),
|
||||
subPort: z.number().int().min(0).max(65535),
|
||||
subProfileUrl: z.string(),
|
||||
subRoutingRules: z.string(),
|
||||
subShowIdentityOnAllLinks: z.boolean(),
|
||||
subSupportUrl: z.string(),
|
||||
subThemeDir: z.string(),
|
||||
subTitle: z.string(),
|
||||
subURI: z.string(),
|
||||
subUpdates: z.number().int().min(0).max(525600),
|
||||
tgBotAPIServer: z.string(),
|
||||
tgBotBackup: z.boolean(),
|
||||
tgBotChatId: z.string(),
|
||||
tgBotEnable: z.boolean(),
|
||||
tgBotProxy: z.string(),
|
||||
tgBotToken: z.string(),
|
||||
tgCpu: z.number().int().min(0).max(100),
|
||||
tgEnabledEvents: z.string(),
|
||||
tgLang: z.string(),
|
||||
tgMemory: z.number().int().min(0).max(100),
|
||||
tgRunTime: z.string(),
|
||||
timeLocation: z.string(),
|
||||
trafficDiff: z.number().int().min(0).max(100),
|
||||
trustedProxyCIDRs: z.string(),
|
||||
twoFactorEnable: z.boolean(),
|
||||
twoFactorToken: z.string(),
|
||||
warpUpdateInterval: z.number().int().min(0),
|
||||
webBasePath: z.string(),
|
||||
webCertFile: z.string(),
|
||||
@@ -136,71 +84,27 @@ export type AllSetting = z.infer<typeof AllSettingSchema>;
|
||||
export const AllSettingViewSchema = z.object({
|
||||
datepicker: z.string(),
|
||||
expireDiff: z.number().int().min(0),
|
||||
externalTrafficInformEnable: z.boolean(),
|
||||
externalTrafficInformURI: z.string(),
|
||||
hasApiToken: z.boolean(),
|
||||
hasLdapPassword: z.boolean(),
|
||||
hasNordSecret: z.boolean(),
|
||||
hasSmtpPassword: z.boolean(),
|
||||
hasTgBotToken: z.boolean(),
|
||||
hasTwoFactorToken: z.boolean(),
|
||||
hasWarpSecret: z.boolean(),
|
||||
ipLimitAllowlist: z.string(),
|
||||
ldapAutoCreate: z.boolean(),
|
||||
ldapAutoDelete: z.boolean(),
|
||||
ldapBaseDN: z.string(),
|
||||
ldapBindDN: z.string(),
|
||||
ldapDefaultExpiryDays: z.number().int().min(0),
|
||||
ldapDefaultLimitIP: z.number().int().min(0),
|
||||
ldapDefaultTotalGB: z.number().int().min(0),
|
||||
ldapEnable: z.boolean(),
|
||||
ldapFlagField: z.string(),
|
||||
ldapHost: z.string(),
|
||||
ldapInboundTags: z.string(),
|
||||
ldapInsecureSkipVerify: z.boolean(),
|
||||
ldapInvertFlag: z.boolean(),
|
||||
ldapPassword: z.string(),
|
||||
ldapPort: z.number().int().min(0).max(65535),
|
||||
ldapSyncCron: z.string(),
|
||||
ldapTruthyValues: z.string(),
|
||||
ldapUseTLS: z.boolean(),
|
||||
ldapUserAttr: z.string(),
|
||||
ldapUserFilter: z.string(),
|
||||
ldapVlessField: z.string(),
|
||||
outboundDownThreshold: z.number().int().min(1).max(100),
|
||||
pageSize: z.number().int().min(0).max(1000),
|
||||
panelOutbound: z.string(),
|
||||
remarkTemplate: z.string(),
|
||||
restartXrayOnClientDisable: z.boolean(),
|
||||
sessionMaxAge: z.number().int().min(1).max(525600),
|
||||
smtpCpu: z.number().int().min(0).max(100),
|
||||
smtpEnable: z.boolean(),
|
||||
smtpEnabledEvents: z.string(),
|
||||
smtpEncryptionType: z.string(),
|
||||
smtpFrom: z.string(),
|
||||
smtpFromName: z.string(),
|
||||
smtpHost: z.string(),
|
||||
smtpMemory: z.number().int().min(0).max(100),
|
||||
smtpPassword: z.string(),
|
||||
smtpPort: z.number().int().min(1).max(65535),
|
||||
smtpTo: z.string(),
|
||||
smtpUsername: z.string(),
|
||||
subAnnounce: z.string(),
|
||||
subCertFile: z.string(),
|
||||
subClashAutoDetect: z.boolean(),
|
||||
subClashEnable: z.boolean(),
|
||||
subClashEnableRouting: z.boolean(),
|
||||
subClashPath: z.string(),
|
||||
subClashRules: z.string(),
|
||||
subClashURI: z.string(),
|
||||
subClashUserAgentRegex: z.string(),
|
||||
subDomain: z.string(),
|
||||
subEnable: z.boolean(),
|
||||
subEnableRouting: z.boolean(),
|
||||
subEncrypt: z.boolean(),
|
||||
subHideSettings: z.boolean(),
|
||||
subIncyEnableRouting: z.boolean(),
|
||||
subIncyRoutingRules: z.string(),
|
||||
subJsonAlwaysArray: z.boolean(),
|
||||
subJsonAutoDetect: z.boolean(),
|
||||
subJsonEnable: z.boolean(),
|
||||
@@ -208,7 +112,6 @@ export const AllSettingViewSchema = z.object({
|
||||
subJsonMux: z.string(),
|
||||
subJsonObservatory: z.string(),
|
||||
subJsonPath: z.string(),
|
||||
subJsonRules: z.string(),
|
||||
subJsonURI: z.string(),
|
||||
subJsonUserAgentRegex: z.string(),
|
||||
subKeyFile: z.string(),
|
||||
@@ -216,29 +119,15 @@ export const AllSettingViewSchema = z.object({
|
||||
subPath: z.string(),
|
||||
subPort: z.number().int().min(0).max(65535),
|
||||
subProfileUrl: z.string(),
|
||||
subRoutingRules: z.string(),
|
||||
subShowIdentityOnAllLinks: z.boolean(),
|
||||
subSupportUrl: z.string(),
|
||||
subThemeDir: z.string(),
|
||||
subTitle: z.string(),
|
||||
subURI: z.string(),
|
||||
subUpdates: z.number().int().min(0).max(525600),
|
||||
tgBotAPIServer: z.string(),
|
||||
tgBotBackup: z.boolean(),
|
||||
tgBotChatId: z.string(),
|
||||
tgBotEnable: z.boolean(),
|
||||
tgBotProxy: z.string(),
|
||||
tgBotToken: z.string(),
|
||||
tgCpu: z.number().int().min(0).max(100),
|
||||
tgEnabledEvents: z.string(),
|
||||
tgLang: z.string(),
|
||||
tgMemory: z.number().int().min(0).max(100),
|
||||
tgRunTime: z.string(),
|
||||
timeLocation: z.string(),
|
||||
trafficDiff: z.number().int().min(0).max(100),
|
||||
trustedProxyCIDRs: z.string(),
|
||||
twoFactorEnable: z.boolean(),
|
||||
twoFactorToken: z.string(),
|
||||
warpUpdateInterval: z.number().int().min(0),
|
||||
webBasePath: z.string(),
|
||||
webCertFile: z.string(),
|
||||
@@ -249,13 +138,6 @@ export const AllSettingViewSchema = z.object({
|
||||
});
|
||||
export type AllSettingView = z.infer<typeof AllSettingViewSchema>;
|
||||
|
||||
export const AmneziaWGLogsSchema = z.object({
|
||||
events: z.array(z.string()),
|
||||
peers: z.array(z.lazy(() => PeerActivitySchema)),
|
||||
running: z.boolean(),
|
||||
});
|
||||
export type AmneziaWGLogs = z.infer<typeof AmneziaWGLogsSchema>;
|
||||
|
||||
export const ApiTokenSchema = z.object({
|
||||
createdAt: z.number().int(),
|
||||
enabled: z.boolean(),
|
||||
@@ -643,9 +525,10 @@ export const NodeMutationRequestSchema = z.object({
|
||||
inboundSyncMode: z.enum(['all', 'selected']),
|
||||
inboundTags: z.array(z.string()),
|
||||
name: z.string(),
|
||||
nodeType: z.enum(['traditional', 'agent']).optional(),
|
||||
outboundTag: z.string(),
|
||||
pinnedCertSha256: z.string(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
port: z.number().int().min(0).max(65535),
|
||||
remark: z.string(),
|
||||
scheme: z.enum(['http', 'https']),
|
||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
||||
@@ -678,6 +561,7 @@ export const NodeViewSchema = z.object({
|
||||
name: z.string(),
|
||||
netDown: z.number().int(),
|
||||
netUp: z.number().int(),
|
||||
nodeType: z.string().optional(),
|
||||
onlineCount: z.number().int(),
|
||||
outboundTag: z.string(),
|
||||
panelVersion: z.string(),
|
||||
@@ -714,20 +598,6 @@ export const PanelUpdateStatusSchema = z.object({
|
||||
});
|
||||
export type PanelUpdateStatus = z.infer<typeof PanelUpdateStatusSchema>;
|
||||
|
||||
export const PeerActivitySchema = z.object({
|
||||
allowedIPs: z.string(),
|
||||
down: z.number().int(),
|
||||
email: z.string(),
|
||||
endpoint: z.string(),
|
||||
handshake: z.number().int(),
|
||||
inboundId: z.number().int(),
|
||||
interface: z.string(),
|
||||
online: z.boolean(),
|
||||
tag: z.string(),
|
||||
up: z.number().int(),
|
||||
});
|
||||
export type PeerActivity = z.infer<typeof PeerActivitySchema>;
|
||||
|
||||
export const ProbeResultUISchema = z.object({
|
||||
cpuPct: z.number(),
|
||||
error: z.string(),
|
||||
|
||||
@@ -7,46 +7,15 @@ import { parseMsg } from '@/utils/zodValidate';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import {
|
||||
OutboundTrafficListSchema,
|
||||
OutboundTestResultListSchema,
|
||||
XrayConfigPayloadSchema,
|
||||
XraySettingsValueSchema,
|
||||
type OutboundTestResult,
|
||||
type OutboundTrafficRow,
|
||||
} from '@/schemas/xray';
|
||||
|
||||
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||
// One HTTP-mode batch request tests this many outbounds through a single
|
||||
// shared temp xray instance; chunking keeps responses bounded (~30s worst
|
||||
// case — each probe is a cold plus a warm request) and lands Test All
|
||||
// results progressively.
|
||||
const HTTP_BATCH_CHUNK = 16;
|
||||
|
||||
function normalizeOutboundTestUrl(url: string) {
|
||||
return url || DEFAULT_TEST_URL;
|
||||
}
|
||||
|
||||
export function isUdpOutbound(outbound: unknown): boolean {
|
||||
const o = outbound as
|
||||
| { protocol?: string; streamSettings?: { network?: string } }
|
||||
| null
|
||||
| undefined;
|
||||
const p = o?.protocol;
|
||||
const n = o?.streamSettings?.network;
|
||||
return p === 'wireguard' || p === 'hysteria' || n === 'hysteria' || n === 'kcp' || n === 'quic';
|
||||
}
|
||||
|
||||
export type OutboundTestMode = 'tcp' | 'http' | 'real';
|
||||
|
||||
export type { OutboundTrafficRow, OutboundTestResult };
|
||||
export type { OutboundTrafficRow };
|
||||
|
||||
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||
|
||||
export interface OutboundTestState {
|
||||
testing?: boolean;
|
||||
result?: OutboundTestResult | null;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export type SetTemplate = (
|
||||
next: XraySettingsValue | null | ((prev: XraySettingsValue | null) => XraySettingsValue | null),
|
||||
) => void;
|
||||
@@ -60,30 +29,14 @@ export interface UseXraySettingResult {
|
||||
setXraySetting: (next: string) => void;
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
outboundTestUrl: string;
|
||||
setOutboundTestUrl: (v: string) => void;
|
||||
inboundTags: string[];
|
||||
clientReverseTags: string[];
|
||||
subscriptionOutbounds: unknown[];
|
||||
subscriptionOutboundTags: string[];
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
outboundTestStates: Record<number, OutboundTestState>;
|
||||
subscriptionTestStates: Record<string, OutboundTestState>;
|
||||
testingAll: boolean;
|
||||
fetchAll: () => Promise<void>;
|
||||
fetchOutboundsTraffic: () => Promise<void>;
|
||||
resetOutboundsTraffic: (tag: string) => Promise<void>;
|
||||
testOutbound: (
|
||||
index: number,
|
||||
outbound: unknown,
|
||||
mode?: string,
|
||||
) => Promise<OutboundTestResult | null>;
|
||||
testSubscriptionOutbound: (
|
||||
tag: string,
|
||||
outbound: unknown,
|
||||
mode?: string,
|
||||
) => Promise<OutboundTestResult | null>;
|
||||
testAllOutbounds: (mode?: string) => Promise<void>;
|
||||
saveAll: () => Promise<void>;
|
||||
resetToDefault: () => Promise<void>;
|
||||
}
|
||||
@@ -136,9 +89,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const [xraySetting, setXraySettingState] = useState('');
|
||||
const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
|
||||
const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL);
|
||||
const [savedXraySetting, setSavedXraySetting] = useState('');
|
||||
const [savedOutboundTestUrl, setSavedOutboundTestUrl] = useState(DEFAULT_TEST_URL);
|
||||
const config = configQuery.data;
|
||||
const inboundTags = useMemo(() => config?.inboundTags || [], [config]);
|
||||
const clientReverseTags = useMemo(() => config?.clientReverseTags || [], [config]);
|
||||
@@ -147,21 +98,10 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
[config],
|
||||
);
|
||||
const subscriptionOutboundTags = useMemo(() => config?.subscriptionOutboundTags || [], [config]);
|
||||
const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>(
|
||||
{},
|
||||
);
|
||||
// Subscription outbounds aren't in templateSettings.outbounds, so their test
|
||||
// results are keyed by tag rather than by index.
|
||||
const [subscriptionTestStates, setSubscriptionTestStates] = useState<
|
||||
Record<string, OutboundTestState>
|
||||
>({});
|
||||
const [testingAll, setTestingAll] = useState(false);
|
||||
|
||||
const syncingRef = useRef(false);
|
||||
const xraySettingRef = useRef('');
|
||||
const outboundTestUrlRef = useRef(outboundTestUrl);
|
||||
const savedXraySettingRef = useRef(savedXraySetting);
|
||||
const savedOutboundTestUrlRef = useRef(savedOutboundTestUrl);
|
||||
const templateSettingsRef = useRef<XraySettingsValue | null>(null);
|
||||
const subscriptionOutboundsRef = useRef<unknown[]>([]);
|
||||
|
||||
@@ -169,9 +109,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
useEffect(() => {
|
||||
xraySettingRef.current = xraySetting;
|
||||
outboundTestUrlRef.current = outboundTestUrl;
|
||||
savedXraySettingRef.current = savedXraySetting;
|
||||
savedOutboundTestUrlRef.current = savedOutboundTestUrl;
|
||||
templateSettingsRef.current = templateSettings;
|
||||
subscriptionOutboundsRef.current = subscriptionOutbounds;
|
||||
});
|
||||
@@ -180,17 +118,12 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
// of the previous config after a refetch. Local edits win over the refetch.
|
||||
if (config && config !== syncedConfig) {
|
||||
setSyncedConfig(config);
|
||||
const isDirty =
|
||||
savedXraySetting !== xraySetting ||
|
||||
savedOutboundTestUrl !== normalizeOutboundTestUrl(outboundTestUrl);
|
||||
const isDirty = savedXraySetting !== xraySetting;
|
||||
if (!isDirty) {
|
||||
const pretty = JSON.stringify(config.xraySetting, null, 2);
|
||||
const nextUrl = normalizeOutboundTestUrl(config.outboundTestUrl || '');
|
||||
setXraySettingState(pretty);
|
||||
setTemplateSettingsState(config.xraySetting);
|
||||
setSavedXraySetting(pretty);
|
||||
setOutboundTestUrlState(nextUrl);
|
||||
setSavedOutboundTestUrl(nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +159,6 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setOutboundTestUrl = useCallback((v: string) => {
|
||||
setOutboundTestUrlState(v);
|
||||
}, []);
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||
}, [queryClient]);
|
||||
@@ -241,17 +170,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const saveMut = useMutation({
|
||||
mutationFn: async () => {
|
||||
const sentXraySetting = xraySettingRef.current;
|
||||
const sentTestUrl = normalizeOutboundTestUrl(outboundTestUrlRef.current);
|
||||
const msg = await HttpUtil.post('/panel/api/xray/update', {
|
||||
xraySetting: sentXraySetting,
|
||||
outboundTestUrl: sentTestUrl,
|
||||
});
|
||||
return { msg, sentXraySetting, sentTestUrl };
|
||||
return { msg, sentXraySetting };
|
||||
},
|
||||
onSuccess: ({ msg, sentXraySetting, sentTestUrl }) => {
|
||||
onSuccess: ({ msg, sentXraySetting }) => {
|
||||
if (!msg?.success) return;
|
||||
setSavedXraySetting(sentXraySetting);
|
||||
setSavedOutboundTestUrl(sentTestUrl);
|
||||
queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||
},
|
||||
});
|
||||
@@ -291,193 +217,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const spinning = saveMut.isPending || resetDefaultMut.isPending;
|
||||
|
||||
// Shared POST + parse for a batch of outbound tests. The backend probes the
|
||||
// whole batch through one shared temp xray instance and returns results in
|
||||
// request order; this aligns them by index and shapes failures so every
|
||||
// input gets an OutboundTestResult.
|
||||
const postOutboundTestBatch = useCallback(
|
||||
async (outbounds: unknown[], effMode: string): Promise<OutboundTestResult[]> => {
|
||||
const failAll = (error: string): OutboundTestResult[] =>
|
||||
outbounds.map(() => ({ success: false, error, mode: effMode }));
|
||||
try {
|
||||
const raw = await HttpUtil.post('/panel/api/xray/testOutbounds', {
|
||||
outbounds: JSON.stringify(outbounds),
|
||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||
mode: effMode,
|
||||
});
|
||||
const msg = parseMsg(raw, OutboundTestResultListSchema, 'xray/testOutbounds');
|
||||
if (!msg?.success || !Array.isArray(msg.obj)) return failAll(msg?.msg || 'Unknown error');
|
||||
const list = msg.obj;
|
||||
return outbounds.map(
|
||||
(_ob, i) => list[i] ?? { success: false, error: 'Missing result', mode: effMode },
|
||||
);
|
||||
} catch (e) {
|
||||
return failAll(String(e));
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const testOutbound = useCallback(
|
||||
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||
if (!outbound) return null;
|
||||
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
|
||||
setOutboundTestStates((prev) => ({
|
||||
...prev,
|
||||
[index]: { testing: true, result: null, mode: effMode },
|
||||
}));
|
||||
const [result] = await postOutboundTestBatch([outbound], effMode);
|
||||
setOutboundTestStates((prev) => ({ ...prev, [index]: { testing: false, result } }));
|
||||
return result.success ? result : null;
|
||||
},
|
||||
[postOutboundTestBatch],
|
||||
);
|
||||
|
||||
// Test a subscription outbound (not present in templateSettings.outbounds);
|
||||
// results are keyed by tag in subscriptionTestStates.
|
||||
const testSubscriptionOutbound = useCallback(
|
||||
async (tag: string, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||
if (!outbound || !tag) return null;
|
||||
const effMode = mode === 'tcp' && isUdpOutbound(outbound) ? 'http' : mode;
|
||||
setSubscriptionTestStates((prev) => ({
|
||||
...prev,
|
||||
[tag]: { testing: true, result: null, mode: effMode },
|
||||
}));
|
||||
const [result] = await postOutboundTestBatch([outbound], effMode);
|
||||
setSubscriptionTestStates((prev) => ({ ...prev, [tag]: { testing: false, result } }));
|
||||
return result.success ? result : null;
|
||||
},
|
||||
[postOutboundTestBatch],
|
||||
);
|
||||
|
||||
const testAllOutbounds = useCallback(
|
||||
async (mode = 'tcp') => {
|
||||
// Template outbounds key their results by index (outboundTestStates);
|
||||
// subscription outbounds aren't in the template, so they key by tag
|
||||
// (subscriptionTestStates). Both go through the same probe endpoint.
|
||||
const templateList = templateSettingsRef.current?.outbounds || [];
|
||||
const subList = (subscriptionOutboundsRef.current || []) as Array<{
|
||||
tag?: string;
|
||||
protocol?: string;
|
||||
}>;
|
||||
if ((templateList.length === 0 && subList.length === 0) || testingAll) return;
|
||||
setTestingAll(true);
|
||||
try {
|
||||
type TcpEntry =
|
||||
| { kind: 'tpl'; index: number; outbound: unknown }
|
||||
| { kind: 'sub'; tag: string; outbound: unknown };
|
||||
const tcpQueue: TcpEntry[] = [];
|
||||
// HTTP batches stay homogeneous (all template or all subscription) so a
|
||||
// tag shared between a template and a subscription outbound can't collide
|
||||
// inside one batch, and each batch's results route to one state map.
|
||||
const probeMode = mode === 'real' ? 'real' : 'http';
|
||||
const httpTplQueue: { index: number; outbound: unknown }[] = [];
|
||||
const httpSubQueue: { tag: string; outbound: unknown }[] = [];
|
||||
const enqueue = (
|
||||
ob: { tag?: string; protocol?: string },
|
||||
kind: 'tpl' | 'sub',
|
||||
index: number,
|
||||
tag: string,
|
||||
) => {
|
||||
const proto = ob?.protocol;
|
||||
if (proto === 'blackhole' || proto === 'loopback' || ob?.tag === 'blocked') return;
|
||||
// freedom ("direct") and dns aren't proxies — skip them in every mode.
|
||||
if (proto === 'freedom' || proto === 'dns') return;
|
||||
if (kind === 'sub' && !tag) return;
|
||||
const toHttp = mode !== 'tcp' || isUdpOutbound(ob);
|
||||
if (kind === 'tpl') {
|
||||
if (toHttp) httpTplQueue.push({ index, outbound: ob });
|
||||
else tcpQueue.push({ kind: 'tpl', index, outbound: ob });
|
||||
} else if (toHttp) {
|
||||
httpSubQueue.push({ tag, outbound: ob });
|
||||
} else {
|
||||
tcpQueue.push({ kind: 'sub', tag, outbound: ob });
|
||||
}
|
||||
};
|
||||
templateList.forEach((ob, i) => enqueue(ob, 'tpl', i, ''));
|
||||
subList.forEach((ob) => enqueue(ob, 'sub', -1, typeof ob?.tag === 'string' ? ob.tag : ''));
|
||||
|
||||
// TCP probes are dial-only and cheap server-side; per-item requests
|
||||
// keep results landing one by one, each routed to its own state map.
|
||||
const runTcpLane = async () => {
|
||||
const queue = [...tcpQueue];
|
||||
const worker = async () => {
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) break;
|
||||
if (item.kind === 'sub')
|
||||
await testSubscriptionOutbound(item.tag, item.outbound, mode);
|
||||
else await testOutbound(item.index, item.outbound, mode);
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(8, queue.length) }, () => worker()));
|
||||
};
|
||||
// HTTP probes go out as chunked batches — one temp xray spawn per
|
||||
// chunk instead of one per outbound, with results landing per chunk.
|
||||
const runTplHttpLane = async () => {
|
||||
for (let at = 0; at < httpTplQueue.length; at += HTTP_BATCH_CHUNK) {
|
||||
const chunk = httpTplQueue.slice(at, at + HTTP_BATCH_CHUNK);
|
||||
setOutboundTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const item of chunk)
|
||||
next[item.index] = { testing: true, result: null, mode: probeMode };
|
||||
return next;
|
||||
});
|
||||
const results = await postOutboundTestBatch(
|
||||
chunk.map((c) => c.outbound),
|
||||
probeMode,
|
||||
);
|
||||
setOutboundTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
chunk.forEach((item, i) => {
|
||||
next[item.index] = { testing: false, result: results[i] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
const runSubHttpLane = async () => {
|
||||
for (let at = 0; at < httpSubQueue.length; at += HTTP_BATCH_CHUNK) {
|
||||
const chunk = httpSubQueue.slice(at, at + HTTP_BATCH_CHUNK);
|
||||
setSubscriptionTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const item of chunk)
|
||||
next[item.tag] = { testing: true, result: null, mode: probeMode };
|
||||
return next;
|
||||
});
|
||||
const results = await postOutboundTestBatch(
|
||||
chunk.map((c) => c.outbound),
|
||||
probeMode,
|
||||
);
|
||||
setSubscriptionTestStates((prev) => {
|
||||
const next = { ...prev };
|
||||
chunk.forEach((item, i) => {
|
||||
next[item.tag] = { testing: false, result: results[i] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
// HTTP batches must not overlap: the backend serialises them with a
|
||||
// non-blocking lock and rejects a second concurrent batch ("Another
|
||||
// outbound test is already running"). Run the template and subscription
|
||||
// HTTP lanes one after the other; TCP probes don't take that lock, so
|
||||
// they still run alongside.
|
||||
const runHttpLane = async () => {
|
||||
await runTplHttpLane();
|
||||
await runSubHttpLane();
|
||||
};
|
||||
await Promise.all([runTcpLane(), runHttpLane()]);
|
||||
} finally {
|
||||
setTestingAll(false);
|
||||
}
|
||||
},
|
||||
[testingAll, testOutbound, testSubscriptionOutbound, postOutboundTestBatch],
|
||||
);
|
||||
|
||||
const saveDisabled =
|
||||
savedXraySetting === xraySetting &&
|
||||
savedOutboundTestUrl === normalizeOutboundTestUrl(outboundTestUrl);
|
||||
const saveDisabled = savedXraySetting === xraySetting;
|
||||
|
||||
const outboundsTraffic = useMemo(() => trafficQuery.data ?? [], [trafficQuery.data]);
|
||||
|
||||
@@ -491,22 +231,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
setXraySetting,
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
setOutboundTestUrl,
|
||||
inboundTags,
|
||||
clientReverseTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
fetchAll,
|
||||
fetchOutboundsTraffic: fetchOutboundsTrafficCb,
|
||||
resetOutboundsTraffic,
|
||||
testOutbound,
|
||||
testSubscriptionOutbound,
|
||||
testAllOutbounds,
|
||||
saveAll,
|
||||
resetToDefault,
|
||||
}),
|
||||
@@ -519,22 +251,14 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
setXraySetting,
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
setOutboundTestUrl,
|
||||
inboundTags,
|
||||
clientReverseTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
fetchAll,
|
||||
fetchOutboundsTrafficCb,
|
||||
resetOutboundsTraffic,
|
||||
testOutbound,
|
||||
testSubscriptionOutbound,
|
||||
testAllOutbounds,
|
||||
saveAll,
|
||||
resetToDefault,
|
||||
],
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
.xray-page .restart-icon {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.xray-page .restart-result {
|
||||
max-width: 480px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.xray-page .advanced-meta h4 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
@@ -55,21 +55,13 @@ export default function XrayPage() {
|
||||
setXraySetting,
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
setOutboundTestUrl,
|
||||
inboundTags,
|
||||
clientReverseTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
fetchAll,
|
||||
resetOutboundsTraffic,
|
||||
testOutbound,
|
||||
testSubscriptionOutbound,
|
||||
testAllOutbounds,
|
||||
saveAll,
|
||||
resetToDefault,
|
||||
} = xs;
|
||||
@@ -99,16 +91,6 @@ export default function XrayPage() {
|
||||
[setTemplateSettings],
|
||||
);
|
||||
|
||||
async function onTestOutbound(idx: number, mode: string) {
|
||||
const outbound = templateSettings?.outbounds?.[idx];
|
||||
if (outbound) await testOutbound(idx, outbound, mode);
|
||||
}
|
||||
|
||||
async function onTestSubscription(outbound: Record<string, unknown>, mode: string) {
|
||||
const tag = typeof outbound?.tag === 'string' ? outbound.tag : '';
|
||||
if (tag) await testSubscriptionOutbound(tag, outbound, mode);
|
||||
}
|
||||
|
||||
function onAddOutbound(outbound: Record<string, unknown>) {
|
||||
mutate((tt) => {
|
||||
if (!Array.isArray(tt.outbounds)) tt.outbounds = [];
|
||||
@@ -196,17 +178,11 @@ export default function XrayPage() {
|
||||
templateSettings={templateSettings}
|
||||
setTemplateSettings={setTemplateSettings}
|
||||
outboundsTraffic={outboundsTraffic}
|
||||
outboundTestStates={outboundTestStates}
|
||||
subscriptionTestStates={subscriptionTestStates}
|
||||
testingAll={testingAll}
|
||||
inboundTags={inboundTags}
|
||||
subscriptionOutbounds={subscriptionOutbounds}
|
||||
subscriptionOutboundTags={subscriptionOutboundTags}
|
||||
isMobile={isMobile}
|
||||
onResetTraffic={resetOutboundsTraffic}
|
||||
onTest={onTestOutbound}
|
||||
onTestSubscription={onTestSubscription}
|
||||
onTestAll={testAllOutbounds}
|
||||
onShowWarp={() => setWarpOpen(true)}
|
||||
onShowNord={() => setNordOpen(true)}
|
||||
onShowPia={() => setPiaOpen(true)}
|
||||
@@ -218,8 +194,6 @@ export default function XrayPage() {
|
||||
<BasicsTab
|
||||
templateSettings={templateSettings}
|
||||
setTemplateSettings={setTemplateSettings}
|
||||
outboundTestUrl={outboundTestUrl}
|
||||
onChangeOutboundTestUrl={setOutboundTestUrl}
|
||||
onResetDefault={resetToDefault}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,16 +29,12 @@ import {
|
||||
interface BasicsTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
outboundTestUrl: string;
|
||||
onChangeOutboundTestUrl: (v: string) => void;
|
||||
onResetDefault: () => void;
|
||||
}
|
||||
|
||||
export default function BasicsTab({
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundTestUrl,
|
||||
onChangeOutboundTestUrl,
|
||||
onResetDefault,
|
||||
}: BasicsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -276,18 +272,6 @@ export default function BasicsTab({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
title={t('pages.xray.outboundTestUrl')}
|
||||
description={t('pages.xray.outboundTestUrlDesc')}
|
||||
paddings="small"
|
||||
control={
|
||||
<Input
|
||||
value={outboundTestUrl}
|
||||
onChange={(e) => onChangeOutboundTestUrl(e.target.value)}
|
||||
placeholder="https://www.google.com/generate_204"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { CloudOutlined } from '@ant-design/icons';
|
||||
|
||||
interface CountryPillProps {
|
||||
flag: string;
|
||||
name: string;
|
||||
warp?: string;
|
||||
}
|
||||
|
||||
export default function CountryPill({ flag, name, warp }: CountryPillProps) {
|
||||
const isWarp = !!warp && warp.toLowerCase() !== 'off';
|
||||
return (
|
||||
<span className={isWarp ? 'country-pill warp-on' : 'country-pill'}>
|
||||
{isWarp && <CloudOutlined className="warp-cloud-icon" />}
|
||||
{flag && <span>{flag}</span>}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
@@ -7,127 +6,34 @@ import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
ThunderboltOutlined,
|
||||
LoadingOutlined,
|
||||
ExportOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { SizeFormatter } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import type {
|
||||
OutboundTestMode,
|
||||
OutboundTestState,
|
||||
OutboundTrafficRow,
|
||||
} from '@/hooks/useXraySetting';
|
||||
import type { OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
import CountryPill from './CountryPill';
|
||||
import TestResultPopover from './TestResultPopover';
|
||||
import {
|
||||
countryFlag,
|
||||
countryName,
|
||||
isTesting,
|
||||
isUntestable,
|
||||
outboundAddresses,
|
||||
showSecurity,
|
||||
testResult,
|
||||
trafficFor,
|
||||
} from './outbounds-tab-helpers';
|
||||
import { outboundAddresses, showSecurity, trafficFor } from './outbounds-tab-helpers';
|
||||
|
||||
interface OutboundCardListProps {
|
||||
rows: OutboundRow[];
|
||||
testMode: OutboundTestMode;
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
outboundTestStates: Record<number, OutboundTestState>;
|
||||
setFirst: (idx: number) => void;
|
||||
openEdit: (idx: number) => void;
|
||||
onResetTraffic: (tag: string) => void;
|
||||
confirmDelete: (idx: number) => void;
|
||||
onTest: (index: number, mode: string) => void;
|
||||
}
|
||||
|
||||
export default function OutboundCardList({
|
||||
rows,
|
||||
testMode,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
setFirst,
|
||||
openEdit,
|
||||
onResetTraffic,
|
||||
confirmDelete,
|
||||
onTest,
|
||||
}: OutboundCardListProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showEgressIp, setShowEgressIp] = useState<Record<string, boolean>>({});
|
||||
|
||||
const setCardEgressVisible = (key: string, visible: boolean) => {
|
||||
setShowEgressIp((prev) => ({ ...prev, [key]: visible }));
|
||||
};
|
||||
|
||||
const renderEgress = (testKey: number, rowKey: string) => {
|
||||
const result = testResult(outboundTestStates, testKey);
|
||||
const egress = result?.egress;
|
||||
const isEgressVisible = !!showEgressIp[rowKey];
|
||||
const flag = countryFlag(egress?.country);
|
||||
const name = countryName(egress?.country, i18n.language);
|
||||
const addresses = [
|
||||
egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null,
|
||||
egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null,
|
||||
].filter((item): item is { label: string; value: string } => Boolean(item));
|
||||
|
||||
if (!egress || (addresses.length === 0 && !egress.country)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-egress">
|
||||
<div className="card-egress-row">
|
||||
<span>{t('pages.xray.outbound.egress')}:</span>
|
||||
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
||||
{isEgressVisible ? (
|
||||
<EyeOutlined
|
||||
className="ip-toggle-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('pages.index.toggleIpVisibility')}
|
||||
onClick={() => setCardEgressVisible(rowKey, false)}
|
||||
onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, false))}
|
||||
/>
|
||||
) : (
|
||||
<EyeInvisibleOutlined
|
||||
className="ip-toggle-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('pages.index.toggleIpVisibility')}
|
||||
onClick={() => setCardEgressVisible(rowKey, true)}
|
||||
onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, true))}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
{egress.country && (
|
||||
<CountryPill flag={flag} name={name || egress.country} warp={egress.warp} />
|
||||
)}
|
||||
</div>
|
||||
{addresses.map((addr) => (
|
||||
<Tooltip key={addr.label} title={addr.value}>
|
||||
<div className="card-egress-row">
|
||||
<span className="egress-family">{addr.label}:</span>
|
||||
<span
|
||||
className={
|
||||
isEgressVisible ? 'address-visible egress-ip' : 'address-hidden egress-ip'
|
||||
}
|
||||
>
|
||||
{addr.value}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
@@ -219,7 +125,6 @@ export default function OutboundCardList({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{renderEgress(record.key, String(record.key))}
|
||||
<div className="card-foot">
|
||||
<span className="traffic-up">
|
||||
↑ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).up)}
|
||||
@@ -228,23 +133,6 @@ export default function OutboundCardList({
|
||||
<span className="traffic-down">
|
||||
↓ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).down)}
|
||||
</span>
|
||||
<span className="card-test">
|
||||
{testResult(outboundTestStates, record.key) ? (
|
||||
<TestResultPopover result={testResult(outboundTestStates, record.key)!} />
|
||||
) : isTesting(outboundTestStates, record.key) ? (
|
||||
<LoadingOutlined />
|
||||
) : null}
|
||||
<Button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
size="small"
|
||||
loading={isTesting(outboundTestStates, record.key)}
|
||||
disabled={isUntestable(record) || isTesting(outboundTestStates, record.key)}
|
||||
icon={<ThunderboltOutlined />}
|
||||
aria-label={t('check')}
|
||||
onClick={() => onTest(record.key, testMode)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -75,51 +75,8 @@
|
||||
background: var(--ant-color-fill-tertiary);
|
||||
}
|
||||
|
||||
.egress-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.egress-ip {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.egress-stack {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.egress-address {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.egress-family {
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.country-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.country-pill.warp-on {
|
||||
border-color: #f48120;
|
||||
}
|
||||
|
||||
.warp-cloud-icon {
|
||||
width: 1em;
|
||||
@@ -185,30 +142,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-egress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.card-egress-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-test {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.traffic-up {
|
||||
color: #008771;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Row,
|
||||
Space,
|
||||
Switch,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
ApiOutlined,
|
||||
MoreOutlined,
|
||||
RetweetOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
@@ -47,13 +45,7 @@ import { propagateOutboundTagRename } from '../basics/helpers';
|
||||
import { planOutboundDeletion, applyOutboundDeletion } from '../reference-cleanup';
|
||||
import DeletionImpactList from '../DeletionImpactList';
|
||||
import { isBalancerLoopbackTag } from '../balancers/balancer-loopback';
|
||||
import type {
|
||||
XraySettingsValue,
|
||||
SetTemplate,
|
||||
OutboundTestMode,
|
||||
OutboundTestState,
|
||||
OutboundTrafficRow,
|
||||
} from '@/hooks/useXraySetting';
|
||||
import type { XraySettingsValue, SetTemplate, OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
import './OutboundsTab.css';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
@@ -82,17 +74,11 @@ interface OutboundsTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
outboundTestStates: Record<number, OutboundTestState>;
|
||||
subscriptionTestStates: Record<string, OutboundTestState>;
|
||||
testingAll: boolean;
|
||||
inboundTags: string[];
|
||||
subscriptionOutbounds?: unknown[];
|
||||
subscriptionOutboundTags?: string[];
|
||||
isMobile: boolean;
|
||||
onResetTraffic: (tag: string) => void;
|
||||
onTest: (index: number, mode: string) => void;
|
||||
onTestSubscription: (outbound: Record<string, unknown>, mode: string) => void;
|
||||
onTestAll: (mode: string) => void;
|
||||
onShowWarp: () => void;
|
||||
onShowNord: () => void;
|
||||
onShowPia: () => void;
|
||||
@@ -103,17 +89,11 @@ export default function OutboundsTab({
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
subscriptionTestStates,
|
||||
testingAll,
|
||||
inboundTags: _inboundTags,
|
||||
subscriptionOutbounds,
|
||||
subscriptionOutboundTags,
|
||||
isMobile,
|
||||
onResetTraffic,
|
||||
onTest,
|
||||
onTestSubscription,
|
||||
onTestAll,
|
||||
onShowWarp,
|
||||
onShowNord,
|
||||
onShowPia,
|
||||
@@ -122,7 +102,6 @@ export default function OutboundsTab({
|
||||
const { t } = useTranslation();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [testMode, setTestMode] = useState<OutboundTestMode>('tcp');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingOutbound, setEditingOutbound] = useState<Record<string, unknown> | null>(null);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
@@ -519,17 +498,14 @@ export default function OutboundsTab({
|
||||
}
|
||||
|
||||
const columns = useOutboundColumns({
|
||||
testMode,
|
||||
rows,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
openEdit,
|
||||
setFirst,
|
||||
moveUp,
|
||||
moveDown,
|
||||
confirmDelete,
|
||||
onResetTraffic,
|
||||
onTest,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -581,26 +557,6 @@ export default function OutboundsTab({
|
||||
</Col>
|
||||
<Col xs={24} sm={12} className="toolbar-right">
|
||||
<Space size="small" wrap>
|
||||
<Tooltip title={t('pages.xray.outbound.testModeTooltip')}>
|
||||
<Radio.Group
|
||||
value={testMode}
|
||||
onChange={(e) => setTestMode(e.target.value)}
|
||||
buttonStyle="solid"
|
||||
size="small"
|
||||
>
|
||||
<Radio.Button value="tcp">TCP</Radio.Button>
|
||||
<Radio.Button value="http">HTTP</Radio.Button>
|
||||
<Radio.Button value="real">{t('pages.xray.outbound.modeRealDelay')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={testingAll}
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => onTestAll(testMode)}
|
||||
>
|
||||
{!isMobile && t('pages.xray.outbound.testAll')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
placement="topRight"
|
||||
okText={t('reset')}
|
||||
@@ -617,14 +573,11 @@ export default function OutboundsTab({
|
||||
{isMobile ? (
|
||||
<OutboundCardList
|
||||
rows={rows}
|
||||
testMode={testMode}
|
||||
outboundsTraffic={outboundsTraffic}
|
||||
outboundTestStates={outboundTestStates}
|
||||
setFirst={setFirst}
|
||||
openEdit={openEdit}
|
||||
onResetTraffic={onResetTraffic}
|
||||
confirmDelete={confirmDelete}
|
||||
onTest={onTest}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
@@ -675,10 +628,7 @@ export default function OutboundsTab({
|
||||
<SubscriptionOutbounds
|
||||
subscriptionOutbounds={subscriptionOutbounds}
|
||||
outboundsTraffic={outboundsTraffic}
|
||||
subscriptionTestStates={subscriptionTestStates}
|
||||
testMode={testMode}
|
||||
isMobile={isMobile}
|
||||
onTestSubscription={onTestSubscription}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
@@ -1,49 +1,28 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Table, Tag, Tooltip } from 'antd';
|
||||
import { ThunderboltOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Table, Tag, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import { SizeFormatter } from '@/utils';
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import type {
|
||||
OutboundTestMode,
|
||||
OutboundTestState,
|
||||
OutboundTrafficRow,
|
||||
} from '@/hooks/useXraySetting';
|
||||
import type { OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
import TestResultPopover from './TestResultPopover';
|
||||
import {
|
||||
effectiveTestMode,
|
||||
isTesting,
|
||||
isUntestable,
|
||||
outboundAddresses,
|
||||
showSecurity,
|
||||
testModeLabel,
|
||||
testResult,
|
||||
trafficFor,
|
||||
} from './outbounds-tab-helpers';
|
||||
import { outboundAddresses, showSecurity, trafficFor } from './outbounds-tab-helpers';
|
||||
|
||||
interface SubscriptionOutboundsProps {
|
||||
subscriptionOutbounds: unknown[];
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
subscriptionTestStates: Record<string, OutboundTestState>;
|
||||
testMode: OutboundTestMode;
|
||||
isMobile: boolean;
|
||||
onTestSubscription: (outbound: Record<string, unknown>, mode: string) => void;
|
||||
}
|
||||
|
||||
// Read-only view of outbounds imported from active subscriptions. They are not
|
||||
// part of the editable template (so no edit/delete/move), but traffic is matched
|
||||
// by tag and they can be latency-tested via the same backend endpoint.
|
||||
// by tag.
|
||||
export default function SubscriptionOutbounds({
|
||||
subscriptionOutbounds,
|
||||
outboundsTraffic,
|
||||
subscriptionTestStates,
|
||||
testMode,
|
||||
isMobile,
|
||||
onTestSubscription,
|
||||
}: SubscriptionOutboundsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -104,36 +83,6 @@ export default function SubscriptionOutbounds({
|
||||
);
|
||||
};
|
||||
|
||||
const latencyCell = (record: OutboundRow) => {
|
||||
const key = record.tag || '';
|
||||
const r = testResult(subscriptionTestStates, key);
|
||||
if (!r)
|
||||
return isTesting(subscriptionTestStates, key) ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<span className="empty">—</span>
|
||||
);
|
||||
return <TestResultPopover result={r} />;
|
||||
};
|
||||
|
||||
const testButton = (record: OutboundRow) => {
|
||||
const key = record.tag || '';
|
||||
return (
|
||||
<Tooltip title={`${t('check')} (${testModeLabel(effectiveTestMode(record, testMode), t)})`}>
|
||||
<Button
|
||||
aria-label={t('check')}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
size={isMobile ? 'small' : undefined}
|
||||
loading={isTesting(subscriptionTestStates, key)}
|
||||
disabled={!record.tag || isUntestable(record) || isTesting(subscriptionTestStates, key)}
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => onTestSubscription(record as unknown as Record<string, unknown>, testMode)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="subscription-outbounds-head">
|
||||
<div className="subscription-outbounds-title">
|
||||
@@ -154,13 +103,9 @@ export default function SubscriptionOutbounds({
|
||||
<span className="card-num">{index + 1}</span>
|
||||
{identityCell(record)}
|
||||
</div>
|
||||
{testButton(record)}
|
||||
</div>
|
||||
{outboundAddresses(record).length > 0 && addressCell(record)}
|
||||
<div className="card-foot">
|
||||
{trafficCell(record)}
|
||||
<span className="card-test">{latencyCell(record)}</span>
|
||||
</div>
|
||||
<div className="card-foot">{trafficCell(record)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -194,20 +139,6 @@ export default function SubscriptionOutbounds({
|
||||
width: 200,
|
||||
render: (_v, record) => trafficCell(record),
|
||||
},
|
||||
{
|
||||
title: t('pages.nodes.latency'),
|
||||
key: 'testResult',
|
||||
align: 'left',
|
||||
width: 140,
|
||||
render: (_v, record) => latencyCell(record),
|
||||
},
|
||||
{
|
||||
title: t('check'),
|
||||
key: 'test',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (_v, record) => testButton(record),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Popover } from 'antd';
|
||||
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
||||
|
||||
import type { OutboundTestResult } from '@/hooks/useXraySetting';
|
||||
|
||||
import { testModeLabel } from './outbounds-tab-helpers';
|
||||
|
||||
interface TestResultPopoverProps {
|
||||
result: OutboundTestResult;
|
||||
// Custom trigger element; defaults to the ok/fail latency pill.
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// Latency pill + detail popover for an outbound test result: per-endpoint
|
||||
// dial outcomes for TCP probes, HTTP status and the timing breakdown for
|
||||
// HTTP probes.
|
||||
export default function TestResultPopover({ result: r, children }: TestResultPopoverProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const breakdown: Array<{ key: string; label: string; value: string }> = [];
|
||||
if (typeof r.httpStatus === 'number') {
|
||||
breakdown.push({
|
||||
key: 'status',
|
||||
label: t('pages.xray.outbound.httpStatus'),
|
||||
value: String(r.httpStatus),
|
||||
});
|
||||
}
|
||||
if (typeof r.connectMs === 'number') {
|
||||
breakdown.push({
|
||||
key: 'connect',
|
||||
label: t('pages.xray.outbound.breakdownConnect'),
|
||||
value: `${r.connectMs} ms`,
|
||||
});
|
||||
}
|
||||
if (typeof r.tlsMs === 'number') {
|
||||
breakdown.push({
|
||||
key: 'tls',
|
||||
label: t('pages.xray.outbound.breakdownTls'),
|
||||
value: `${r.tlsMs} ms`,
|
||||
});
|
||||
}
|
||||
if (typeof r.ttfbMs === 'number') {
|
||||
breakdown.push({
|
||||
key: 'ttfb',
|
||||
label: t('pages.xray.outbound.breakdownTtfb'),
|
||||
value: `${r.ttfbMs} ms`,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
rootClassName="outbound-test-popover"
|
||||
content={
|
||||
<div className="timing-breakdown">
|
||||
<div className={`td-head ${r.success ? 'ok' : 'fail'}`}>
|
||||
{r.success ? <span>{r.delay} ms</span> : <span>{r.error || 'failed'}</span>}
|
||||
{r.mode && <span className="mode-badge">{testModeLabel(String(r.mode), t)}</span>}
|
||||
</div>
|
||||
{(r.endpoints || []).map((ep) => (
|
||||
<div key={ep.address} className="endpoint-row">
|
||||
<span className={ep.success ? 'dot-ok' : 'dot-fail'}>●</span>
|
||||
<span className="ep-addr">{ep.address}</span>
|
||||
<span className="ep-meta">
|
||||
{ep.success ? `${ep.delay} ms` : ep.error || 'failed'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{breakdown.map((row) => (
|
||||
<div key={row.key} className="breakdown-row">
|
||||
<span className="bd-label">{row.label}</span>
|
||||
<span className="bd-value">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children ?? (
|
||||
<span className={r.success ? 'pill-ok' : 'pill-fail'}>
|
||||
{r.success ? <CheckCircleFilled /> : <CloseCircleFilled />}
|
||||
{r.success ? <span>{r.delay} ms</span> : <span>failed</span>}
|
||||
</span>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import { isUdpOutbound } from '@/hooks/useXraySetting';
|
||||
import type {
|
||||
OutboundTestMode,
|
||||
OutboundTestState,
|
||||
OutboundTrafficRow,
|
||||
} from '@/hooks/useXraySetting';
|
||||
import type { OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
|
||||
@@ -54,33 +47,10 @@ export function outboundAddresses(o: OutboundRow): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function isUntestable(o: OutboundRow): boolean {
|
||||
if (!o) return true;
|
||||
if (
|
||||
o.protocol === Protocols.Blackhole ||
|
||||
o.protocol === Protocols.Loopback ||
|
||||
o.tag === 'blocked'
|
||||
)
|
||||
return true;
|
||||
// freedom ("direct") and dns aren't proxies — a TCP dial has no endpoint and
|
||||
// an HTTP probe would only measure the host's own direct reachability, so
|
||||
// they're untestable in every mode.
|
||||
if (o.protocol === Protocols.Freedom || o.protocol === Protocols.DNS) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showSecurity(security?: string): boolean {
|
||||
return security === 'tls' || security === 'reality';
|
||||
}
|
||||
|
||||
export function effectiveTestMode(o: unknown, mode: OutboundTestMode): OutboundTestMode {
|
||||
return mode === 'tcp' && isUdpOutbound(o) ? 'http' : mode;
|
||||
}
|
||||
|
||||
export function testModeLabel(mode: string, t: TFunction): string {
|
||||
return mode === 'real' ? t('pages.xray.outbound.modeRealDelay') : mode.toUpperCase();
|
||||
}
|
||||
|
||||
export function trafficFor(
|
||||
outboundsTraffic: OutboundTrafficRow[],
|
||||
o: OutboundRow,
|
||||
@@ -106,17 +76,3 @@ export function countryName(country?: string, locale?: string): string {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTesting<K extends string | number>(
|
||||
states: Record<K, OutboundTestState>,
|
||||
idx: K,
|
||||
): boolean {
|
||||
return !!states?.[idx]?.testing;
|
||||
}
|
||||
|
||||
export function testResult<K extends string | number>(
|
||||
states: Record<K, OutboundTestState>,
|
||||
idx: K,
|
||||
) {
|
||||
return states?.[idx]?.result || null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
@@ -7,69 +7,40 @@ import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
ThunderboltOutlined,
|
||||
LoadingOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import { SizeFormatter } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import type {
|
||||
OutboundTestMode,
|
||||
OutboundTestState,
|
||||
OutboundTrafficRow,
|
||||
} from '@/hooks/useXraySetting';
|
||||
import type { OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
import CountryPill from './CountryPill';
|
||||
import TestResultPopover from './TestResultPopover';
|
||||
import {
|
||||
effectiveTestMode,
|
||||
countryFlag,
|
||||
countryName,
|
||||
isTesting,
|
||||
isUntestable,
|
||||
outboundAddresses,
|
||||
showSecurity,
|
||||
testModeLabel,
|
||||
testResult,
|
||||
trafficFor,
|
||||
} from './outbounds-tab-helpers';
|
||||
import { outboundAddresses, showSecurity, trafficFor } from './outbounds-tab-helpers';
|
||||
|
||||
interface OutboundColumnsParams {
|
||||
testMode: OutboundTestMode;
|
||||
rows: OutboundRow[];
|
||||
outboundsTraffic: OutboundTrafficRow[];
|
||||
outboundTestStates: Record<number, OutboundTestState>;
|
||||
openEdit: (idx: number) => void;
|
||||
setFirst: (idx: number) => void;
|
||||
moveUp: (idx: number) => void;
|
||||
moveDown: (idx: number) => void;
|
||||
confirmDelete: (idx: number) => void;
|
||||
onResetTraffic: (tag: string) => void;
|
||||
onTest: (index: number, mode: string) => void;
|
||||
}
|
||||
|
||||
export function useOutboundColumns({
|
||||
testMode,
|
||||
rows,
|
||||
outboundsTraffic,
|
||||
outboundTestStates,
|
||||
openEdit,
|
||||
setFirst,
|
||||
moveUp,
|
||||
moveDown,
|
||||
confirmDelete,
|
||||
onResetTraffic,
|
||||
onTest,
|
||||
}: OutboundColumnsParams): ColumnsType<OutboundRow> {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showEgressIp, setShowEgressIp] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -204,94 +175,6 @@ export function useOutboundColumns({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className="egress-header">
|
||||
{t('pages.xray.outbound.egress')}
|
||||
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
||||
{showEgressIp ? (
|
||||
<EyeOutlined
|
||||
className="ip-toggle-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('pages.index.toggleIpVisibility')}
|
||||
onClick={() => setShowEgressIp(false)}
|
||||
onKeyDown={activateOnKey(() => setShowEgressIp(false))}
|
||||
/>
|
||||
) : (
|
||||
<EyeInvisibleOutlined
|
||||
className="ip-toggle-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('pages.index.toggleIpVisibility')}
|
||||
onClick={() => setShowEgressIp(true)}
|
||||
onKeyDown={activateOnKey(() => setShowEgressIp(true))}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
key: 'egress',
|
||||
align: 'left',
|
||||
width: 210,
|
||||
render: (_v, record) => {
|
||||
const egress = testResult(outboundTestStates, record.key)?.egress;
|
||||
const addresses = [
|
||||
egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null,
|
||||
egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null,
|
||||
].filter((item): item is { label: string; value: string } => Boolean(item));
|
||||
if (addresses.length === 0) {
|
||||
return (
|
||||
<Tooltip title={t('pages.xray.outbound.egressHint')}>
|
||||
<span className="empty">—</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="egress-stack">
|
||||
{addresses.map((addr) => (
|
||||
<Tooltip key={addr.label} title={addr.value}>
|
||||
<span className="egress-address">
|
||||
<span className="egress-family">{addr.label}</span>
|
||||
<span
|
||||
className={
|
||||
showEgressIp ? 'address-visible egress-ip' : 'address-hidden egress-ip'
|
||||
}
|
||||
>
|
||||
{addr.value}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.outbound.country'),
|
||||
key: 'egressCountry',
|
||||
align: 'left',
|
||||
width: 160,
|
||||
render: (_v, record) => {
|
||||
const egress = testResult(outboundTestStates, record.key)?.egress;
|
||||
if (!egress?.country) {
|
||||
return (
|
||||
<Tooltip title={t('pages.xray.outbound.egressHint')}>
|
||||
<span className="empty">—</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const flag = countryFlag(egress.country);
|
||||
const name = countryName(egress.country, i18n.language);
|
||||
return (
|
||||
<Tooltip
|
||||
title={egress.warp ? `Cloudflare trace · WARP ${egress.warp}` : 'Cloudflare trace'}
|
||||
>
|
||||
<CountryPill flag={flag} name={name || egress.country} warp={egress.warp} />
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.inbounds.traffic'),
|
||||
key: 'traffic',
|
||||
@@ -308,45 +191,8 @@ export function useOutboundColumns({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.nodes.latency'),
|
||||
key: 'testResult',
|
||||
align: 'left',
|
||||
width: 140,
|
||||
render: (_v, record) => {
|
||||
const r = testResult(outboundTestStates, record.key);
|
||||
if (!r)
|
||||
return isTesting(outboundTestStates, record.key) ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<span className="empty">—</span>
|
||||
);
|
||||
return <TestResultPopover result={r} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('check'),
|
||||
key: 'test',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (_v, record) => (
|
||||
<Tooltip
|
||||
title={`${t('check')} (${testModeLabel(effectiveTestMode(record, testMode), t)})`}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
loading={isTesting(outboundTestStates, record.key)}
|
||||
disabled={isUntestable(record) || isTesting(outboundTestStates, record.key)}
|
||||
icon={<ThunderboltOutlined />}
|
||||
aria-label={t('check')}
|
||||
onClick={() => onTest(record.key, testMode)}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[t, i18n.language, testMode, rows, outboundTestStates, outboundsTraffic, showEgressIp],
|
||||
[t, rows, outboundsTraffic],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ export const XrayConfigPayloadSchema = z
|
||||
xraySetting: XraySettingsValueSchema,
|
||||
inboundTags: z.array(z.string()).optional(),
|
||||
clientReverseTags: z.array(z.string()).optional(),
|
||||
outboundTestUrl: z.string().optional(),
|
||||
// Subscription outbounds are injected at runtime (not persisted in xraySetting).
|
||||
// They are provided here so the UI can display them and use their tags in
|
||||
// balancers / routing rules.
|
||||
@@ -67,47 +66,6 @@ export const OutboundTrafficRowSchema = z.object({
|
||||
|
||||
export const OutboundTrafficListSchema = z.array(OutboundTrafficRowSchema);
|
||||
|
||||
export const OutboundTestResultSchema = z
|
||||
.object({
|
||||
tag: z.string().optional(),
|
||||
success: z.boolean(),
|
||||
delay: z.number().optional(),
|
||||
error: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
// HTTP-mode extras: status answered by the test URL plus the httptrace
|
||||
// timing breakdown (dial to local inbound / target TLS via the outbound /
|
||||
// time to first byte).
|
||||
httpStatus: z.number().optional(),
|
||||
connectMs: z.number().optional(),
|
||||
tlsMs: z.number().optional(),
|
||||
ttfbMs: z.number().optional(),
|
||||
endpoints: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
address: z.string(),
|
||||
delay: z.number().optional(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.loose(),
|
||||
)
|
||||
.optional(),
|
||||
egress: z
|
||||
.object({
|
||||
ipv4: z.string().optional(),
|
||||
ipv6: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
warp: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
// Batch results from /xray/testOutbounds, aligned with the request order.
|
||||
export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
|
||||
|
||||
export const RuleFormSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
domain: z.string(),
|
||||
@@ -148,4 +106,3 @@ export type RuleFormValues = z.infer<typeof RuleFormSchema>;
|
||||
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||
export type XrayConfigPayload = z.infer<typeof XrayConfigPayloadSchema>;
|
||||
export type OutboundTrafficRow = z.infer<typeof OutboundTrafficRowSchema>;
|
||||
export type OutboundTestResult = z.infer<typeof OutboundTestResultSchema>;
|
||||
|
||||
@@ -25,8 +25,6 @@ describe('BasicsTab malformed happyEyeballs', () => {
|
||||
<BasicsTab
|
||||
templateSettings={settingsWithMalformedHappyEyeballs()}
|
||||
setTemplateSettings={vi.fn()}
|
||||
outboundTestUrl=""
|
||||
onChangeOutboundTestUrl={vi.fn()}
|
||||
onResetDefault={vi.fn()}
|
||||
/>,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import OutboundsTab from '@/pages/xray/outbounds/OutboundsTab';
|
||||
@@ -17,79 +17,72 @@ function settingsWithHiddenLoopback(): XraySettingsValue {
|
||||
} as unknown as XraySettingsValue;
|
||||
}
|
||||
|
||||
// The visible rows hide the balancer-loopback outbound, so every row action
|
||||
// must translate the positional row index back to the outbound's real index
|
||||
// in the full array. "Move to top" on the second visible row (proxy-b, real
|
||||
// index 2) must move proxy-b — not the hidden loopback at positional index 1.
|
||||
describe('OutboundsTab hidden-loopback index mapping', () => {
|
||||
it('probes the outbound at its real array index, not the positional row index', () => {
|
||||
const onTest = vi.fn();
|
||||
function renderTab(isMobile: boolean) {
|
||||
const setTemplateSettings = vi.fn();
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
renderWithProviders(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OutboundsTab
|
||||
templateSettings={settingsWithHiddenLoopback()}
|
||||
setTemplateSettings={vi.fn()}
|
||||
setTemplateSettings={setTemplateSettings}
|
||||
outboundsTraffic={[]}
|
||||
outboundTestStates={{}}
|
||||
subscriptionTestStates={{}}
|
||||
testingAll={false}
|
||||
inboundTags={[]}
|
||||
isMobile={false}
|
||||
isMobile={isMobile}
|
||||
onResetTraffic={vi.fn()}
|
||||
onTest={onTest}
|
||||
onTestSubscription={vi.fn()}
|
||||
onTestAll={vi.fn()}
|
||||
onShowWarp={vi.fn()}
|
||||
onShowNord={vi.fn()}
|
||||
onShowPia={vi.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return setTemplateSettings;
|
||||
}
|
||||
|
||||
function expectMovedProxyBToTop(setTemplateSettings: ReturnType<typeof vi.fn>) {
|
||||
expect(setTemplateSettings).toHaveBeenCalled();
|
||||
const updater = setTemplateSettings.mock.calls.at(-1)?.[0];
|
||||
expect(typeof updater).toBe('function');
|
||||
const next = updater(structuredClone(settingsWithHiddenLoopback()));
|
||||
expect(next.outbounds.map((o: { tag?: string }) => o.tag)).toEqual([
|
||||
'proxy-b',
|
||||
'proxy-a',
|
||||
'_bl_bal1',
|
||||
]);
|
||||
}
|
||||
|
||||
async function clickMoveToTop(scope: Element) {
|
||||
const moreButton = scope.querySelector('button[aria-label="more"]') as HTMLButtonElement;
|
||||
expect(moreButton).toBeTruthy();
|
||||
fireEvent.click(moreButton);
|
||||
const item = await waitFor(() => {
|
||||
const found = [...document.querySelectorAll('.ant-dropdown-menu-item')].find((el) =>
|
||||
el.textContent?.includes('Move to top'),
|
||||
);
|
||||
expect(found).toBeTruthy();
|
||||
return found as HTMLElement;
|
||||
});
|
||||
fireEvent.click(item);
|
||||
}
|
||||
|
||||
it('acts on the real array index from the desktop table', async () => {
|
||||
const setTemplateSettings = renderTab(false);
|
||||
const tbody = document.querySelector('.ant-table-tbody');
|
||||
const tableRows = tbody?.querySelectorAll('tr.ant-table-row') ?? [];
|
||||
expect(tableRows.length).toBe(2);
|
||||
|
||||
const checkButton = tableRows[1].querySelector(
|
||||
'button[aria-label="Check"]',
|
||||
) as HTMLButtonElement;
|
||||
fireEvent.click(checkButton);
|
||||
|
||||
expect(onTest).toHaveBeenCalledTimes(1);
|
||||
expect(onTest.mock.calls[0][0]).toBe(2);
|
||||
await clickMoveToTop(tableRows[1]);
|
||||
expectMovedProxyBToTop(setTemplateSettings);
|
||||
});
|
||||
|
||||
it('probes the real array index from the mobile card list as well', () => {
|
||||
const onTest = vi.fn();
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
renderWithProviders(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OutboundsTab
|
||||
templateSettings={settingsWithHiddenLoopback()}
|
||||
setTemplateSettings={vi.fn()}
|
||||
outboundsTraffic={[]}
|
||||
outboundTestStates={{}}
|
||||
subscriptionTestStates={{}}
|
||||
testingAll={false}
|
||||
inboundTags={[]}
|
||||
isMobile
|
||||
onResetTraffic={vi.fn()}
|
||||
onTest={onTest}
|
||||
onTestSubscription={vi.fn()}
|
||||
onTestAll={vi.fn()}
|
||||
onShowWarp={vi.fn()}
|
||||
onShowNord={vi.fn()}
|
||||
onShowPia={vi.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
it('acts on the real array index from the mobile card list as well', async () => {
|
||||
const setTemplateSettings = renderTab(true);
|
||||
const cards = document.querySelectorAll('.outbound-card');
|
||||
expect(cards.length).toBe(2);
|
||||
|
||||
const checkButton = cards[1].querySelector('button[aria-label="Check"]') as HTMLButtonElement;
|
||||
fireEvent.click(checkButton);
|
||||
|
||||
expect(onTest).toHaveBeenCalledTimes(1);
|
||||
expect(onTest.mock.calls[0][0]).toBe(2);
|
||||
await clickMoveToTop(cards[1]);
|
||||
expectMovedProxyBToTop(setTemplateSettings);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ function xrayPayload(overrides: Record<string, unknown> = {}) {
|
||||
xraySetting: {},
|
||||
inboundTags: [],
|
||||
clientReverseTags: [],
|
||||
outboundTestUrl: 'https://test.example',
|
||||
subscriptionOutbounds: [],
|
||||
subscriptionOutboundTags: [],
|
||||
...overrides,
|
||||
@@ -48,23 +47,4 @@ describe('useXraySetting', () => {
|
||||
await waitFor(() => expect(result.current.subscriptionOutbounds).toEqual([{ tag: 'after' }]));
|
||||
expect(result.current.xraySetting).toBe('{"outbounds":[]}');
|
||||
});
|
||||
|
||||
it('keeps the outbound test URL input empty when it is cleared', async () => {
|
||||
const payload = xrayPayload({ outboundTestUrl: 'https://www.google.com/generate_204' });
|
||||
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
||||
if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
|
||||
return new Msg(true, '');
|
||||
});
|
||||
const queryClient = makeTestQueryClient();
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
const { result } = renderHook(() => useXraySetting(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.fetched).toBe(true));
|
||||
act(() => result.current.setOutboundTestUrl(''));
|
||||
|
||||
expect(result.current.outboundTestUrl).toBe('');
|
||||
expect(result.current.saveDisabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user