// This file is autogenerated, please don't change manually "use strict"; "require baseclass"; "require fs"; "require uci"; "require ui"; "require rpc"; // src/validators/validateIp.ts function isIPv4(ip) { const ipRegex = /^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/; return ipRegex.test(ip); } function isIPv6(ip) { if (!ip.includes(":") || ip.includes("%")) { return false; } try { new URL(`http://[${ip}]/`); return true; } catch (_e) { return false; } } function validateIP(ip) { if (isIPv4(ip) || isIPv6(ip)) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _("Invalid IP address") }; } // src/validators/validateDomain.ts function asciiHostname(hostname) { if (!hostname || /[\s@:/]/.test(hostname) || hostname.startsWith(".") || hostname.endsWith(".")) { return null; } try { return new URL(`http://${hostname}`).hostname; } catch { return null; } } function validAsciiDomain(hostname, requireDot = true) { if (!hostname || hostname.length > 253) { return false; } const parts = hostname.split("."); if (parts.some((part) => !part || part.length > 63)) { return false; } if (parts.some((part) => !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(part))) { return false; } if (!requireDot) { return true; } if (parts.length < 2) { return false; } const tld = parts[parts.length - 1]; return /^(?:[a-z]{2,}|xn--[a-z0-9-]{2,59})$/.test(tld); } function validateDomain(domain, allowDotTLD = false) { const normalized = `${domain || ""}`.trim(); if (allowDotTLD) { const dotTld = normalized.startsWith(".") ? normalized.slice(1) : ""; const ascii2 = asciiHostname(dotTld); if (ascii2 && !ascii2.includes(".") && validAsciiDomain(ascii2, false)) { return { valid: true, message: _("Valid") }; } } const slashIndex = normalized.indexOf("/"); const hostname = slashIndex >= 0 ? normalized.slice(0, slashIndex) : normalized; const path = slashIndex >= 0 ? normalized.slice(slashIndex) : ""; if (path && /\s/.test(path)) { return { valid: false, message: _("Invalid domain address") }; } const ascii = asciiHostname(hostname); if (!ascii || !validAsciiDomain(ascii)) { return { valid: false, message: _("Invalid domain address") }; } return { valid: true, message: _("Valid") }; } // src/validators/hostPort.ts function unbracketHost(host) { if (host.startsWith("[") && host.endsWith("]")) { return host.slice(1, -1); } return host; } function isValidHost(host) { const normalizedHost = unbracketHost(host); return isIPv4(normalizedHost) || isIPv6(normalizedHost) || validateDomain(normalizedHost).valid; } function isValidPort(port) { const normalized = String(port ?? ""); const value = Number(normalized); return /^\d+$/.test(normalized) && value >= 1 && value <= 65535; } function parseHostPort(value) { if (!value) { return null; } if (value.startsWith("[")) { const end = value.indexOf("]"); if (end <= 1 || value.slice(end + 1, end + 2) !== ":") { return null; } const parsed2 = { host: value.slice(1, end), port: value.slice(end + 2) }; return isValidHost(parsed2.host) ? parsed2 : null; } const firstColon = value.indexOf(":"); const lastColon = value.lastIndexOf(":"); if (firstColon < 0 || firstColon !== lastColon) { return null; } const parsed = { host: value.slice(0, firstColon), port: value.slice(firstColon + 1) }; return isValidHost(parsed.host) ? parsed : null; } // src/validators/validateDns.ts function validateDNS(value) { if (!value) { return { valid: false, message: _("DNS server address cannot be empty") }; } const [addressPart, ...pathParts] = value.split("/"); const parsedHostPort = parseHostPort(addressPart); const host = parsedHostPort ? parsedHostPort.host : unbracketHost(addressPart); const domainValue = parsedHostPort ? host + (pathParts.length > 0 ? `/${pathParts.join("/")}` : "") : value.replace(/:(\d+)(?=\/|$)/, ""); if (parsedHostPort && !isValidPort(parsedHostPort.port)) { return { valid: false, message: _("Invalid DNS server port") }; } if (validateIP(host).valid) { return { valid: true, message: _("Valid") }; } if (validateDomain(domainValue).valid) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _( "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" ) }; } // src/validators/validateUrl.ts function validateUrl(url, protocols = ["http:", "https:"]) { if (!url.length) { return { valid: false, message: _("Invalid URL format") }; } const hasValidProtocol = protocols.some((p) => url.indexOf(p + "//") === 0); if (!hasValidProtocol) return { valid: false, message: _("URL must use one of the following protocols:") + " " + protocols.join(", ") }; try { const parsed = new URL(url); const host = parsed.hostname.startsWith("[") ? parsed.hostname.slice(1, -1) : parsed.hostname; if ((isValidHost(host) || host === "localhost") && (!parsed.port || isValidPort(parsed.port))) { return { valid: true, message: _("Valid") }; } } catch (_e) { return { valid: false, message: _("Invalid URL format") }; } return { valid: false, message: _("Invalid URL format") }; } // src/validators/validatePath.ts function validatePath(value) { if (!value) { return { valid: false, message: _("Path cannot be empty") }; } const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/; if (pathRegex.test(value)) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _( 'Invalid path format. Path must start with "/" and contain valid characters' ) }; } // src/validators/validateSubnet.ts function validateSubnet(value) { const [ip, cidr, extra] = value.split("/"); if (!ip || extra !== void 0 || !isIPv4(ip) && !isIPv6(ip)) { return { valid: false, message: _("Invalid format. Use an IP address or CIDR subnet") }; } if ((ip === "0.0.0.0" || ip === "::") && cidr == null) { return { valid: false, message: _("Unspecified IP address is not allowed") }; } if (cidr) { if (!/^\d+$/.test(cidr)) { return { valid: false, message: _("Invalid CIDR prefix") }; } const cidrNum = parseInt(cidr, 10); const maxPrefix = isIPv6(ip) ? 128 : 32; if (cidrNum < 0 || cidrNum > maxPrefix) { return { valid: false, message: _( "CIDR must be between 0 and 32 for IPv4 or 0 and 128 for IPv6" ) }; } } return { valid: true, message: _("Valid") }; } // src/validators/bulkValidate.ts function bulkValidate(values, validate) { const results = values.map((value) => ({ ...validate(value), value })); return { valid: results.every((r) => r.valid), results }; } // src/validators/validateOutboundJson.ts var SERVER_OUTBOUND_TYPES = /* @__PURE__ */ new Set([ "vless", "vmess", "trojan", "shadowsocks", "socks", "http", "hysteria2", "hysteria" ]); function invalid(message) { return { valid: false, message }; } function isPlainObject(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function nonEmptyString(value) { return typeof value === "string" && value.trim().length > 0; } function validateServerPort(value) { return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535; } function validateOutbounds(value) { return Array.isArray(value) && value.length > 0 && value.every((item) => nonEmptyString(item)); } function validateOutboundJson(value, usedTags = []) { const normalized = `${value || ""}`.trim(); if (!normalized.length) { return invalid(_("JSON outbound cannot be empty")); } let parsed; try { parsed = JSON.parse(normalized); } catch { return { valid: false, message: _("Invalid JSON format") }; } if (!isPlainObject(parsed)) { return invalid(_("JSON outbound must be a JSON object")); } if (!nonEmptyString(parsed.type)) { return invalid(_("JSON outbound must contain a non-empty type field")); } if (!nonEmptyString(parsed.tag)) { return invalid(_("JSON outbound must contain a non-empty tag field")); } const tag = parsed.tag.trim(); if (usedTags.some((usedTag) => `${usedTag || ""}`.trim() === tag)) { return invalid(_("Duplicate JSON outbound tag")); } const type = parsed.type.trim().toLowerCase(); if ((type === "selector" || type === "urltest") && !validateOutbounds(parsed.outbounds)) { return invalid( _( "Selector and URLTest outbounds must contain a non-empty outbounds array" ) ); } if (SERVER_OUTBOUND_TYPES.has(type)) { if (!nonEmptyString(parsed.server)) { return invalid( _("Server outbound must contain a non-empty server field") ); } if (!validateServerPort(parsed.server_port)) { return invalid( _("Server outbound must contain a numeric server_port from 1 to 65535") ); } } else if (parsed.server_port !== void 0 && !validateServerPort(parsed.server_port)) { return invalid(_("server_port must be a number from 1 to 65535")); } if (parsed.outbounds !== void 0 && !validateOutbounds(parsed.outbounds)) { return invalid(_("outbounds must be a non-empty array of strings")); } if (parsed.detour !== void 0 && !nonEmptyString(parsed.detour)) { return invalid(_("detour must be a non-empty string")); } return { valid: true, message: _("Valid") }; } // src/validators/validateShadowsocksUrl.ts function validateShadowsocksUrl(url) { if (!url.startsWith("ss://")) { return { valid: false, message: _("Invalid Shadowsocks URL: must start with ss://") }; } try { if (!url || /\s/.test(url)) { return { valid: false, message: _("Invalid Shadowsocks URL: must not contain spaces") }; } const mainPart = url.includes("?") ? url.split("?")[0] : url.split("#")[0]; const encryptedPart = mainPart.split("/")[2]?.split("@")[0]; if (!encryptedPart) { return { valid: false, message: _("Invalid Shadowsocks URL: missing credentials") }; } try { const decoded = atob(encryptedPart); if (!decoded.includes(":")) { return { valid: false, message: _( "Invalid Shadowsocks URL: decoded credentials must contain method:password" ) }; } } catch (_e) { if (!encryptedPart.includes(":") && !encryptedPart.includes("-")) { return { valid: false, message: _( 'Invalid Shadowsocks URL: missing method and password separator ":"' ) }; } } const serverPart = url.split("@")[1]; if (!serverPart) { return { valid: false, message: _("Invalid Shadowsocks URL: missing server address") }; } const parsedHostPort = parseHostPort(serverPart); if (!parsedHostPort) { return { valid: false, message: _("Invalid Shadowsocks URL: invalid server and port") }; } const { host: server, port: portAndRest } = parsedHostPort; if (!server) { return { valid: false, message: _("Invalid Shadowsocks URL: missing server") }; } const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; if (!port) { return { valid: false, message: _("Invalid Shadowsocks URL: missing port") }; } if (!isValidPort(port)) { return { valid: false, message: _("Invalid port number. Must be between 1 and 65535") }; } } catch (_e) { return { valid: false, message: _("Invalid Shadowsocks URL: parsing failed") }; } return { valid: true, message: _("Valid") }; } // src/helpers/parseQueryString.ts function parseQueryString(query) { const clean = query.startsWith("?") ? query.slice(1) : query; return clean.split("&").filter(Boolean).reduce( (acc, pair) => { const [rawKey, rawValue = ""] = pair.split("="); if (!rawKey) { return acc; } const key = decodeURIComponent(rawKey); const value = decodeURIComponent(rawValue); return { ...acc, [key]: value }; }, {} ); } // src/validators/validateVlessUrl.ts function validateVlessUrl(url) { try { if (!url.startsWith("vless://")) return { valid: false, message: "Invalid VLESS URL: must start with vless://" }; if (/\s/.test(url)) return { valid: false, message: "Invalid VLESS URL: must not contain spaces" }; const body = url.slice("vless://".length); const [mainPart] = body.split("#"); const [userHostPort, queryString] = mainPart.split("?"); if (!userHostPort) return { valid: false, message: "Invalid VLESS URL: missing host and UUID" }; const [userPart, hostPortPart] = userHostPort.split("@"); if (!userPart) return { valid: false, message: "Invalid VLESS URL: missing UUID" }; if (!hostPortPart) return { valid: false, message: "Invalid VLESS URL: missing server" }; const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) return { valid: false, message: "Invalid VLESS URL: invalid host and port" }; const { host, port } = parsedHostPort; if (!host) return { valid: false, message: "Invalid VLESS URL: missing hostname" }; if (!port) return { valid: false, message: "Invalid VLESS URL: missing port" }; const cleanedPort = port.replace("/", ""); if (!isValidPort(cleanedPort)) return { valid: false, message: "Invalid VLESS URL: invalid port number" }; if (!queryString) return { valid: false, message: "Invalid VLESS URL: missing query parameters" }; const params = parseQueryString(queryString); const validTypes = [ "tcp", "raw", "udp", "grpc", "http", "httpupgrade", "xhttp", "ws", "kcp" ]; const validSecurities = ["tls", "reality", "none"]; const transportType = params.type || "tcp"; if (!validTypes.includes(transportType)) return { valid: false, message: "Invalid VLESS URL: unsupported or missing type" }; if (!params.security || !validSecurities.includes(params.security)) return { valid: false, message: "Invalid VLESS URL: unsupported or missing security" }; if (params.security === "reality") { if (!params.pbk) return { valid: false, message: "Invalid VLESS URL: missing pbk for reality" }; if (!params.fp) return { valid: false, message: "Invalid VLESS URL: missing fp for reality" }; } if (params.flow === "xtls-rprx-vision-udp443") { return { valid: false, message: "Invalid VLESS URL: flow xtls-rprx-vision-udp443 is not supported" }; } return { valid: true, message: _("Valid") }; } catch (_e) { return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; } } // src/validators/validateVmessUrl.ts function decodeBase64Json(value) { const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized.padEnd( normalized.length + (4 - normalized.length % 4) % 4, "=" ); const binary = atob(padded); const decoded = decodeURIComponent( Array.from(binary).map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join("") ); return JSON.parse(decoded); } function validateVmessUrl(url) { try { if (!url.startsWith("vmess://")) { return { valid: false, message: "Invalid VMess URL: must start with vmess://" }; } if (/\s/.test(url)) { return { valid: false, message: "Invalid VMess URL: must not contain spaces" }; } const body = url.slice("vmess://".length); const [encoded] = body.split("#"); if (!encoded) { return { valid: false, message: "Invalid VMess URL: missing encoded config" }; } const config = decodeBase64Json(encoded); if (!config || typeof config !== "object") { return { valid: false, message: "Invalid VMess URL: invalid config" }; } const { add, port, id } = config; if (!add || typeof add !== "string" || !isValidHost(add)) { return { valid: false, message: "Invalid VMess URL: invalid server" }; } if (!isValidPort(port)) { return { valid: false, message: "Invalid VMess URL: invalid port number" }; } if (!id || typeof id !== "string") { return { valid: false, message: "Invalid VMess URL: missing UUID" }; } return { valid: true, message: _("Valid") }; } catch (_e) { return { valid: false, message: _("Invalid VMess URL: parsing failed") }; } } // src/validators/validateTrojanUrl.ts function validateTrojanUrl(url) { try { if (!url.startsWith("trojan://")) { return { valid: false, message: _("Invalid Trojan URL: must start with trojan://") }; } if (!url || /\s/.test(url)) { return { valid: false, message: _("Invalid Trojan URL: must not contain spaces") }; } const body = url.slice("trojan://".length); const [mainPart] = body.split("#"); const [userHostPort] = mainPart.split("?"); const [userPart, hostPortPart] = userHostPort.split("@"); if (!userHostPort) return { valid: false, message: "Invalid Trojan URL: missing credentials and host" }; if (!userPart) return { valid: false, message: "Invalid Trojan URL: missing password" }; if (!hostPortPart) return { valid: false, message: "Invalid Trojan URL: missing hostname and port" }; const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) return { valid: false, message: "Invalid Trojan URL: invalid host and port" }; const { host, port } = parsedHostPort; if (!host) return { valid: false, message: "Invalid Trojan URL: missing hostname" }; if (!port) return { valid: false, message: "Invalid Trojan URL: missing port" }; if (!isValidPort(port)) return { valid: false, message: "Invalid Trojan URL: invalid port number" }; } catch (_e) { return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; } return { valid: true, message: _("Valid") }; } // src/validators/validateSocksUrl.ts function validateSocksUrl(url) { try { if (!/^socks(4|4a|5):\/\//.test(url)) { return { valid: false, message: _( "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" ) }; } if (!url || /\s/.test(url)) { return { valid: false, message: _("Invalid SOCKS URL: must not contain spaces") }; } const body = url.replace(/^socks(4|4a|5):\/\//, ""); const [authAndHost] = body.split("#"); const [credentials, hostPortPart] = authAndHost.includes("@") ? authAndHost.split("@") : [null, authAndHost]; if (credentials) { const [username, _password] = credentials.split(":"); if (!username) { return { valid: false, message: _("Invalid SOCKS URL: missing username") }; } } if (!hostPortPart) { return { valid: false, message: _("Invalid SOCKS URL: missing host and port") }; } const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) { return { valid: false, message: _("Invalid SOCKS URL: invalid host and port") }; } const { host, port } = parsedHostPort; if (!host) { return { valid: false, message: _("Invalid SOCKS URL: missing hostname or IP") }; } if (!port) { return { valid: false, message: _("Invalid SOCKS URL: missing port") }; } if (!isValidPort(port)) { return { valid: false, message: _("Invalid SOCKS URL: invalid port number") }; } } catch (_e) { return { valid: false, message: _("Invalid SOCKS URL: parsing failed") }; } return { valid: true, message: _("Valid") }; } // src/validators/validateHysteriaUrl.ts function validateHysteria2Url(url) { try { const isHY2 = url.startsWith("hysteria2://"); const isHY2Short = url.startsWith("hy2://"); if (!isHY2 && !isHY2Short) { return { valid: false, message: _("Invalid HY2 URL: must start with hysteria2:// or hy2://") }; } if (/\s/.test(url)) { return { valid: false, message: _("Invalid HY2 URL: must not contain spaces") }; } const prefix = isHY2 ? "hysteria2://" : "hy2://"; const body = url.slice(prefix.length); const [mainPart] = body.split("#"); const [authHostPort, queryString] = mainPart.split("?"); if (!authHostPort) return { valid: false, message: _("Invalid HY2 URL: missing credentials/server") }; const [passwordPart, hostPortPart] = authHostPort.split("@"); if (!passwordPart) return { valid: false, message: _("Invalid HY2 URL: missing password") }; if (!hostPortPart) return { valid: false, message: _("Invalid HY2 URL: missing host & port") }; const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) { return { valid: false, message: _("Invalid HY2 URL: invalid host & port") }; } const { host, port } = parsedHostPort; if (!host) { return { valid: false, message: _("Invalid HY2 URL: missing host") }; } if (!port) { return { valid: false, message: _("Invalid HY2 URL: missing port") }; } const cleanedPort = port.replace("/", ""); const portEntries = cleanedPort.split(","); const isValidPortEntry = (entry) => { if (!entry) { return false; } if (!entry.includes("-")) { return isValidPort(entry); } const rangeParts = entry.split("-"); if (rangeParts.length !== 2) { return false; } const [start, end] = rangeParts; return isValidPort(start) && isValidPort(end) && Number(start) <= Number(end); }; if (!portEntries.every(isValidPortEntry)) { return { valid: false, message: _("Invalid HY2 URL: invalid port number") }; } if (queryString) { const params = parseQueryString(queryString); const paramsKeys = Object.keys(params); if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) { return { valid: false, message: _("Invalid HY2 URL: insecure must be 0 or 1") }; } const validObfsTypes = ["none", "salamander"]; if (paramsKeys.includes("obfs") && !validObfsTypes.includes(params.obfs)) { return { valid: false, message: _("Invalid HY2 URL: unsupported obfs type") }; } if (paramsKeys.includes("obfs") && params.obfs !== "none" && !params["obfs-password"]) { return { valid: false, message: _( "Invalid HY2 URL: obfs-password required when obfs is set" ) }; } if (paramsKeys.includes("sni") && !params.sni) { return { valid: false, message: _("Invalid HY2 URL: sni cannot be empty") }; } if (paramsKeys.includes("mport") && (!params.mport || !params.mport.split(",").every(isValidPortEntry))) { return { valid: false, message: _("Invalid HY2 URL: invalid port number") }; } } return { valid: true, message: _("Valid") }; } catch (_e) { return { valid: false, message: _("Invalid HY2 URL: parsing failed") }; } } // src/validators/validateTuicUrl.ts function isValidUuid(value) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( value ); } function validateTuicUrl(url) { try { if (!url.startsWith("tuic://")) { return { valid: false, message: _("Invalid TUIC URL: must start with tuic://") }; } if (/\s/.test(url)) { return { valid: false, message: _("Invalid TUIC URL: must not contain spaces") }; } const body = url.slice("tuic://".length); const [mainPart] = body.split("#"); const [authHostPort, queryString] = mainPart.split("?"); if (!authHostPort) return { valid: false, message: _("Invalid TUIC URL: missing credentials/server") }; const [uuidPasswordPart, hostPortPart] = authHostPort.split("@"); if (!uuidPasswordPart) return { valid: false, message: _("Invalid TUIC URL: missing UUID") }; if (!hostPortPart) return { valid: false, message: _("Invalid TUIC URL: missing host & port") }; const [uuidPart, passwordPart] = uuidPasswordPart.split(":"); const uuid = uuidPart || ""; const _password = passwordPart || ""; if (!uuid) { return { valid: false, message: _("Invalid TUIC URL: missing UUID") }; } if (!isValidUuid(uuid)) { return { valid: false, message: _("Invalid TUIC URL: UUID must be a valid UUID v4 format") }; } const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) { return { valid: false, message: _("Invalid TUIC URL: invalid host & port") }; } const { host, port } = parsedHostPort; if (!host) { return { valid: false, message: _("Invalid TUIC URL: missing host") }; } if (!port) { return { valid: false, message: _("Invalid TUIC URL: missing port") }; } const cleanedPort = port.replace("/", ""); if (!isValidPort(cleanedPort)) { return { valid: false, message: _("Invalid TUIC URL: invalid port number") }; } if (queryString) { const params = parseQueryString(queryString); const paramsKeys = Object.keys(params); const validCc = ["bbr", "cubic", "new_reno"]; if (paramsKeys.includes("congestion_control") && !validCc.includes(params.congestion_control)) { return { valid: false, message: _( "Invalid TUIC URL: congestion_control must be bbr, cubic, or new_reno" ) }; } const validUdpMode = ["native", "quic", "qux"]; if (paramsKeys.includes("udp_relay_mode") && !validUdpMode.includes(params.udp_relay_mode)) { return { valid: false, message: _( "Invalid TUIC URL: udp_relay_mode must be native, quic, or qux" ) }; } if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) { return { valid: false, message: _("Invalid TUIC URL: insecure must be 0 or 1") }; } if (paramsKeys.includes("zero_rtt_handshake") && !["0", "1", "true", "false"].includes(params.zero_rtt_handshake)) { return { valid: false, message: _( "Invalid TUIC URL: zero_rtt_handshake must be 0, 1, true, or false" ) }; } if (paramsKeys.includes("sni") && !params.sni) { return { valid: false, message: _("Invalid TUIC URL: sni cannot be empty") }; } } return { valid: true, message: _("Valid") }; } catch (_e) { return { valid: false, message: _("Invalid TUIC URL: parsing failed") }; } } // src/validators/validateProxyUrl.ts function validateProxyUrl(url) { const trimmedUrl = url.trim(); if (trimmedUrl.startsWith("ss://")) { return validateShadowsocksUrl(trimmedUrl); } if (trimmedUrl.startsWith("vless://")) { return validateVlessUrl(trimmedUrl); } if (trimmedUrl.startsWith("vmess://")) { return validateVmessUrl(trimmedUrl); } if (trimmedUrl.startsWith("trojan://")) { return validateTrojanUrl(trimmedUrl); } if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { return validateHysteria2Url(trimmedUrl); } if (trimmedUrl.startsWith("tuic://")) { return validateTuicUrl(trimmedUrl); } return { valid: false, message: _( "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, hy2://, or tuic://" ) }; } // src/helpers/parseValueList.ts function parseValueList(value) { return value.split(/\n/).map((line) => line.split("//")[0].split("#")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean); } // src/helpers/getProxyUrlName.ts function getProxyUrlName(url) { try { const [_link, hash] = url.split("#"); if (!hash) { return ""; } return decodeURIComponent(hash); } catch { return ""; } } // src/tachyon/tabs/dashboard/partials/renderFlagEmojis.ts var FLAG_EMOJI_PATTERN = /([\u{1f1e6}-\u{1f1ff}]{2}|\u{1f3f4}[\u{e0061}-\u{e007a}]+\u{e007f})/gu; var EXACT_FLAG_EMOJI_PATTERN = /^([\u{1f1e6}-\u{1f1ff}]{2}|\u{1f3f4}[\u{e0061}-\u{e007a}]+\u{e007f})$/u; function renderFlagEmojis(value) { return value.split(FLAG_EMOJI_PATTERN).filter(Boolean).map( (part) => EXACT_FLAG_EMOJI_PATTERN.test(part) ? E("span", { class: "tachyon_dashboard-page__flag-emoji" }, part) : part ); } // src/helpers/downloadAsTxt.ts function downloadAsTxt(text, filename) { const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); const safeName = filename.endsWith(".txt") ? filename : `${filename}.txt`; link.download = safeName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } // src/tachyon/services/logger.service.ts var Logger = class { constructor() { this.logs = []; this.levels = ["debug", "info", "warn", "error"]; } format(level, ...args) { return `[${level.toUpperCase()}] ${args.join(" ")}`; } push(level, ...args) { if (!this.levels.includes(level)) level = "info"; const message = this.format(level, ...args); this.logs.push(message); if (this.logs.length > 2e3) { this.logs.shift(); } switch (level) { case "error": console.error(message); break; case "warn": console.warn(message); break; case "info": console.info(message); break; default: if (typeof localStorage !== "undefined" && localStorage.getItem("tachyon_debug") === "true") { console.log(message); } } } debug(...args) { this.push("debug", ...args); } info(...args) { this.push("info", ...args); } warn(...args) { this.push("warn", ...args); } error(...args) { this.push("error", ...args); } clear() { this.logs = []; } getLogs() { return this.logs.join("\n"); } download(filename = "logs.txt") { if (typeof document === "undefined") { console.warn("Logger.download() \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435"); return; } downloadAsTxt(this.getLogs(), filename); } }; var logger = new Logger(); // src/helpers/withTimeout.ts async function withTimeout(promise, timeoutMs, operationName, timeoutMessage = _("Operation timed out")) { let timeoutId; const start = performance.now(); const timeoutPromise = new Promise((_2, reject) => { timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); }); try { return await Promise.race([promise, timeoutPromise]); } finally { clearTimeout(timeoutId); const elapsed = performance.now() - start; logger.debug("[SHELL]", `[${operationName}] took ${elapsed.toFixed(2)} ms`); } } // src/constants.ts var TACHYON_UCI_PACKAGE = "tachyon"; var TACHYON_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__"; var TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT = "tachyon:action-providers-availability"; var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi"; var IP_CHECK_DOMAIN = "ip.podkop.fyi"; var DEFAULT_LATENCY_TEST_URL = "https://www.gstatic.com/generate_204"; var LATENCY_TEST_URL_OPTIONS = [ DEFAULT_LATENCY_TEST_URL, "https://cp.cloudflare.com/generate_204", "https://captive.apple.com", "https://connectivity-check.ubuntu.com" ]; var DOMAIN_LIST_OPTIONS = { russia_inside: "Russia inside", russia_outside: "Russia outside", ukraine_inside: "Ukraine", geoblock: "Geo Block", block: "Block", porn: "Porn", news: "News", anime: "Anime", youtube: "Youtube", discord: "Discord", meta: "Meta", twitter: "Twitter (X)", hdrezka: "HDRezka", tiktok: "Tik-Tok", telegram: "Telegram", cloudflare: "Cloudflare", google_ai: "Google AI", google_play: "Google Play", hodca: "H.O.D.C.A", roblox: "Roblox", ads_hagezi_pro: "Ads (Hagezi Pro)", supercell: "Supercell", github: "GitHub", hetzner: "Hetzner ASN", ovh: "OVH ASN", digitalocean: "Digital Ocean ASN", cloudfront: "CloudFront ASN" }; var DNS_SERVERS_BY_PROTOCOL = { udp: { "1.1.1.1": "1.1.1.1 (Cloudflare)", "8.8.8.8": "8.8.8.8 (Google)", "9.9.9.9": "9.9.9.9 (Quad9)", "77.88.8.8": "77.88.8.8 (Yandex)", "94.140.14.14": "94.140.14.14 (AdGuard)", "185.222.222.222": "185.222.222.222 (DNS.SB)", "194.242.2.2": "194.242.2.2 (Mullvad)" }, doh: { "https://cloudflare-dns.com/dns-query": "Cloudflare", "https://dns.google/dns-query": "Google", "https://dns.quad9.net/dns-query": "Quad9", "https://dns.adguard.com/dns-query": "AdGuard", "https://dns.mullvad.net/dns-query": "Mullvad", "https://dns.nextdns.io/dns-query": "NextDNS", "https://doh.cleanbrowsing.org/doh/family-filter/": "CleanBrowsing Family" }, dot: { "1.1.1.1": "1.1.1.1 (Cloudflare)", "dns.google": "Google", "dns.quad9.net": "Quad9", "dns.adguard.com": "AdGuard", "dns.mullvad.net": "Mullvad", "dns.nextdns.io": "NextDNS", "common.dot.dns.yandex.net": "Yandex" }, doq: { "1.1.1.1:784": "Cloudflare", "dns.google:784": "Google", "dns.adguard.com:785": "AdGuard", "dns.mullvad.net:784": "Mullvad", "dns.nextdns.io:784": "NextDNS" } }; var BOOTSTRAP_DNS_SERVER_OPTIONS = { "77.88.8.8": "77.88.8.8 (Yandex DNS)", "77.88.8.1": "77.88.8.1 (Yandex DNS)", "1.1.1.1": "1.1.1.1 (Cloudflare DNS)", "1.0.0.1": "1.0.0.1 (Cloudflare DNS)", "8.8.8.8": "8.8.8.8 (Google DNS)", "8.8.4.4": "8.8.4.4 (Google DNS)", "9.9.9.9": "9.9.9.9 (Quad9 DNS)", "9.9.9.11": "9.9.9.11 (Quad9 DNS)" }; var COMMAND_TIMEOUT = 1e4; // src/helpers/executeShellCommand.ts async function executeShellCommand({ command, args, timeout = COMMAND_TIMEOUT }) { try { return await withTimeout( fs.exec(command, args), timeout, [command, ...args].join(" ") ); } catch (err) { const error = err; const code = typeof error?.code === "number" ? error.code : 1; return { stdout: "", stderr: error?.message, code }; } } // src/helpers/onMount.ts function getTarget(target) { if (typeof target === "string") { return document.getElementById(target); } return target; } async function onMount(target) { return new Promise((resolve) => { let observer = null; const resolveIfMountedAndVisible = () => { const mountedTarget = getTarget(target); if (mountedTarget && mountedTarget.isConnected && mountedTarget.offsetParent !== null) { observer?.disconnect(); resolve(mountedTarget); return true; } return false; }; if (resolveIfMountedAndVisible()) { return; } observer = new MutationObserver(() => { resolveIfMountedAndVisible(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden"] }); }); } // src/helpers/getClashApiUrl.ts function getWindowLocation() { return typeof window !== "undefined" ? window.location : void 0; } function canUseDirectClashApi() { const location = getWindowLocation(); return typeof location?.hostname === "string" && location.hostname !== "" && location.protocol !== "https:"; } function getClashWsUrl() { const { hostname } = window.location; return `ws://${hostname}:9090`; } function getClashHttpUrl() { const { hostname } = window.location; return `http://${hostname}:9090`; } function getClashUIUrl() { const { hostname } = window.location; return `http://${hostname}:9090/ui`; } // src/helpers/preserveScrollForPage.ts function preserveScrollForPage(renderFn) { const scrollY = window.scrollY; renderFn(); requestAnimationFrame(() => { window.scrollTo({ top: scrollY }); }); } // src/helpers/svgEl.ts function svgEl(tag, attrs = {}, children = []) { const NS = "http://www.w3.org/2000/svg"; const el = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) { if (v != null) el.setAttribute(k, String(v)); } (Array.isArray(children) ? children : [children]).filter(Boolean).forEach((ch) => el.appendChild(ch)); return el; } // src/helpers/insertIf.ts function insertIf(condition, elements) { return condition ? elements : []; } // src/helpers/isCopyableProxyLink.ts var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i; function isCopyableProxyLink(link) { return COPYABLE_PROXY_URI_RE.test((link || "").trim()); } // src/icons/renderLoaderCircleIcon24.ts function renderLoaderCircleIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-loader-circle rotate" }, [ svgEl("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }), svgEl("animateTransform", { attributeName: "transform", attributeType: "XML", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "1s", repeatCount: "indefinite" }) ] ); } // src/icons/renderCircleAlertIcon24.ts function renderCircleAlertIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-alert-icon lucide-circle-alert" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("line", { x1: "12", y1: "8", x2: "12", y2: "12" }), svgEl("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }) ] ); } // src/icons/renderCircleCheckIcon24.ts function renderCircleCheckIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-check-icon lucide-circle-check" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M9 12l2 2 4-4" }) ] ); } // src/icons/renderCircleSlashIcon24.ts function renderCircleSlashIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-slash-icon lucide-circle-slash" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("line", { x1: "9", y1: "15", x2: "15", y2: "9" }) ] ); } // src/icons/renderCircleXIcon24.ts function renderCircleXIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-x-icon lucide-circle-x" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M15 9L9 15" }), svgEl("path", { d: "M9 9L15 15" }) ] ); } // src/icons/renderCheckIcon24.ts function renderCheckIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-check-icon lucide-check" }, [ svgEl("path", { d: "M20 6 9 17l-5-5" }) ] ); } // src/icons/renderXIcon24.ts function renderXIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-x-icon lucide-x" }, [svgEl("path", { d: "M18 6 6 18" }), svgEl("path", { d: "m6 6 12 12" })] ); } // src/icons/renderTriangleAlertIcon24.ts function renderTriangleAlertIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-triangle-alert-icon lucide-triangle-alert" }, [ svgEl("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }), svgEl("path", { d: "M12 9v4" }), svgEl("path", { d: "M12 17h.01" }) ] ); } // src/icons/renderPauseIcon24.ts function renderPauseIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-pause-icon lucide-pause" }, [ svgEl("rect", { x: "14", y: "3", width: "5", height: "18", rx: "1" }), svgEl("rect", { x: "5", y: "3", width: "5", height: "18", rx: "1" }) ] ); } // src/icons/renderPlayIcon24.ts function renderPlayIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-play-icon lucide-play" }, [ svgEl("path", { d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" }) ] ); } // src/icons/renderRotateCcwIcon24.ts function renderRotateCcwIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" }, [ svgEl("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), svgEl("path", { d: "M3 3v5h5" }) ] ); } // src/icons/renderCircleStopIcon24.ts function renderCircleStopIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-stop-icon lucide-circle-stop" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("rect", { x: "9", y: "9", width: "6", height: "6", rx: "1" }) ] ); } // src/icons/renderCirclePlayIcon24.ts function renderCirclePlayIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-play-icon lucide-circle-play" }, [ svgEl("path", { d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" }), svgEl("circle", { cx: "12", cy: "12", r: "10" }) ] ); } // src/icons/renderCircleCheckBigIcon24.ts function renderCircleCheckBigIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" }, [ svgEl("path", { d: "M21.801 10A10 10 0 1 1 17 3.335" }), svgEl("path", { d: "m9 11 3 3L22 4" }) ] ); } // src/icons/renderSquareChartGanttIcon24.ts function renderSquareChartGanttIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" }, [ svgEl("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), svgEl("path", { d: "M9 8h7" }), svgEl("path", { d: "M8 12h6" }), svgEl("path", { d: "M11 16h5" }) ] ); } // src/icons/renderCogIcon24.ts function renderCogIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-cog-icon lucide-cog" }, [ svgEl("path", { d: "M11 10.27 7 3.34" }), svgEl("path", { d: "m11 13.73-4 6.93" }), svgEl("path", { d: "M12 22v-2" }), svgEl("path", { d: "M12 2v2" }), svgEl("path", { d: "M14 12h8" }), svgEl("path", { d: "m17 20.66-1-1.73" }), svgEl("path", { d: "m17 3.34-1 1.73" }), svgEl("path", { d: "M2 12h2" }), svgEl("path", { d: "m20.66 17-1.73-1" }), svgEl("path", { d: "m20.66 7-1.73 1" }), svgEl("path", { d: "m3.34 17 1.73-1" }), svgEl("path", { d: "m3.34 7 1.73 1" }), svgEl("circle", { cx: "12", cy: "12", r: "2" }), svgEl("circle", { cx: "12", cy: "12", r: "8" }) ] ); } // src/icons/renderSearchIcon24.ts function renderSearchIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-search-icon lucide-search" }, [ svgEl("path", { d: "m21 21-4.34-4.34" }), svgEl("circle", { cx: "11", cy: "11", r: "8" }) ] ); } // src/icons/renderBookOpenTextIcon24.ts function renderBookOpenTextIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-book-open-text-icon lucide-book-open-text" }, [ svgEl("path", { d: "M12 7v14" }), svgEl("path", { d: "M16 12h2" }), svgEl("path", { d: "M16 8h2" }), svgEl("path", { d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" }), svgEl("path", { d: "M6 12h2" }), svgEl("path", { d: "M6 8h2" }) ] ); } // src/icons/renderCopyIcon24.ts function renderCopyIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-copy-icon lucide-copy" }, [ svgEl("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }), svgEl("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }) ] ); } // src/icons/renderLinkIcon24.ts function renderLinkIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-link-icon lucide-link" }, [ svgEl("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), svgEl("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }) ] ); } // src/icons/renderDownloadIcon24.ts function renderDownloadIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-download-icon lucide-download" }, [ svgEl("path", { d: "M12 15V3" }), svgEl("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), svgEl("path", { d: "m7 10 5 5 5-5" }) ] ); } // src/icons/renderInfoIcon24.ts function renderInfoIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-info-icon lucide-info" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M12 16v-4" }), svgEl("path", { d: "M12 8h.01" }) ] ); } // src/icons/renderGlobeIcon24.ts function renderGlobeIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-globe-icon lucide-globe" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }), svgEl("path", { d: "M2 12h20" }) ] ); } // src/helpers/prettyBytes.ts function prettyBytes(n) { const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; if (n < 1e3) { return n + " B"; } const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); n = Number((n / Math.pow(1e3, exponent)).toPrecision(3)); const unit = UNITS[exponent]; return n + " " + unit; } // src/tachyon/tabs/dashboard/partials/renderSections.ts function renderFailedState() { return E( "div", { class: "tachyon_dashboard-page__outbound-section centered", style: "height: 127px" }, E("span", {}, [E("span", {}, _("Dashboard currently unavailable"))]) ); } function renderLoadingState() { return E("div", { id: "dashboard-sections-grid-skeleton", class: "tachyon_dashboard-page__outbound-section skeleton", style: "height: 127px" }); } function isValidHttpUrl(url) { return Boolean(url && /^https?:\/\/\S+$/i.test(url)); } function formatBytes(value) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { return void 0; } return prettyBytes(value); } function formatDate(seconds) { if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) { return void 0; } const date = new Date(seconds * 1e3); if (Number.isNaN(date.getTime())) { return void 0; } return date.toLocaleDateString(void 0, { year: "numeric", month: "2-digit", day: "2-digit" }); } function renderMetadataAction(label, url) { if (!isValidHttpUrl(url)) { return void 0; } return E( "a", { class: "btn tachyon_dashboard-page__subscription-meta__action", href: url, target: "_blank", rel: "noopener noreferrer", title: label, "aria-label": label }, renderLinkIcon24() ); } function renderSubscriptionMetadata(metadata) { if (!metadata || Object.keys(metadata).length <= 1) { return void 0; } const title = metadata.title || metadata.fileName; const traffic = metadata.traffic; const used = formatBytes(traffic?.used) || "0 B"; const total = traffic?.isUnlimited ? "\u221E" : formatBytes(traffic?.total) || "0 B"; const expire = formatDate(metadata.expire); const refillDate = formatDate(metadata.refillDate); const rows = [ traffic ? { label: _("Traffic"), value: `${used} / ${total}` } : void 0, expire ? { label: _("Expires"), value: expire } : void 0, refillDate ? { label: _("Refill"), value: refillDate } : void 0 ].filter(Boolean); const actions = [ renderMetadataAction("Profile", metadata.webPageUrl), renderMetadataAction("Support", metadata.supportUrl), renderMetadataAction("More details", metadata.announceUrl) ].filter(Boolean); return E("div", { class: "tachyon_dashboard-page__subscription-meta" }, [ E("div", { class: "tachyon_dashboard-page__subscription-meta__main" }, [ E( "div", { class: "tachyon_dashboard-page__subscription-meta__heading" }, _("Subscription info:") ), title ? E( "div", { class: "tachyon_dashboard-page__subscription-meta__title" }, title ) : "", rows.length ? E( "div", { class: "tachyon_dashboard-page__subscription-meta__facts" }, rows.map( (row) => E( "div", { class: "tachyon_dashboard-page__subscription-meta__fact" }, [ E( "span", { class: "tachyon_dashboard-page__subscription-meta__fact-key" }, row.label ), E( "span", { class: "tachyon_dashboard-page__subscription-meta__fact-value" }, row.value ) ] ) ) ) : "", actions.length ? E( "div", { class: "tachyon_dashboard-page__subscription-meta__actions" }, actions ) : "" ]), metadata.announce ? E( "blockquote", { class: "tachyon_dashboard-page__subscription-meta__announce" }, metadata.announce ) : "" ]); } function renderSubscriptionUpdateAction(section, subscriptionUpdating, onUpdateSubscription) { if (!section.subscriptionSourceCount) { return void 0; } return E( "button", { type: "button", class: "btn tachyon_dashboard-page__outbound-section__subscription-update", "aria-label": _("Update subscriptions"), disabled: subscriptionUpdating ? true : void 0, click: (event) => { event.preventDefault(); event.stopPropagation(); if (subscriptionUpdating) { return; } onUpdateSubscription(section); } }, subscriptionUpdating ? [renderLoaderCircleIcon24(), _("Update subscriptions")] : _("Update subscriptions") ); } function getLatencyTestLabel(latencyProgress) { const total = Math.trunc(Number(latencyProgress?.total ?? 0)); if (!Number.isFinite(total) || total <= 0) { return _("Test latency"); } const completedValue = Number(latencyProgress?.completed ?? 0); const completed = Number.isFinite(completedValue) ? Math.trunc(completedValue) : 0; return `${_("Test latency")}: ${Math.min( Math.max(0, completed), total )}/${total}`; } function renderDefaultState({ section, onChooseOutbound, onCopyOutbound, onShowUrlTestInfo, onShowPriorityInfo, onTestLatency, onUpdateSubscription, latencyFetching, latencyProgress, subscriptionUpdating, selectorSwitchingTag, isCollapsed, onToggleCollapse }) { const isConnectionNode = ["vpn", "awg", "warp"].includes( section.action || "" ); const isServiceNode = ["zapret", "zapret2", "byedpi"].includes( section.action || "" ); function testLatency() { if (section.withTagSelect) { return onTestLatency( section.latencyTestCodes?.length ? section.latencyTestCodes : section.latencyTestCode || section.code ); } if (section.outbounds.length) { return onTestLatency(section.outbounds[0].code); } } function renderOutbound(outbound) { function getLatencyClass() { if (isConnectionNode) { if (latencyFetching) { return "tachyon_dashboard-page__outbound-grid__item__latency--yellow"; } if (outbound.latency === -1 || !outbound.runtimeAvailable) { return "tachyon_dashboard-page__outbound-grid__item__latency--red"; } if (!outbound.latency) { return "tachyon_dashboard-page__outbound-grid__item__latency--green"; } } if (!outbound.latency) { return "tachyon_dashboard-page__outbound-grid__item__latency--empty"; } if (outbound.latency < 800) { return "tachyon_dashboard-page__outbound-grid__item__latency--green"; } if (outbound.latency < 1500) { return "tachyon_dashboard-page__outbound-grid__item__latency--yellow"; } return "tachyon_dashboard-page__outbound-grid__item__latency--red"; } const connectionStatusText = latencyFetching ? `\u25CF ${_("Checking...")}` : outbound.latency === -1 || !outbound.runtimeAvailable ? `\u25CF N/A` : outbound.latency && outbound.latency > 0 ? `\u25CF ${outbound.latency}ms` : `\u25CF N/A`; const canCopyLink = Boolean(outbound.canCopyLink) || isCopyableProxyLink(outbound.link); const selectorSwitching = Boolean(selectorSwitchingTag); const outboundSwitching = selectorSwitchingTag === outbound.code; const canChooseOutbound = section.withTagSelect && outbound.runtimeAvailable !== false && !selectorSwitching && !outbound.selected; const className = [ "tachyon_dashboard-page__outbound-grid__item", outbound.selected ? "tachyon_dashboard-page__outbound-grid__item--active" : "", canChooseOutbound ? "tachyon_dashboard-page__outbound-grid__item--selectable" : "", section.withTagSelect && !canChooseOutbound ? "tachyon_dashboard-page__outbound-grid__item--disabled" : "", outboundSwitching ? "tachyon_dashboard-page__outbound-grid__item--switching" : "" ].filter(Boolean).join(" "); if (isConnectionNode) { return E( "div", { class: className, style: "display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;" }, [ E( "div", { style: "display: flex; align-items: center; gap: 12px; min-width: 0;" }, [ E( "b", { style: "overflow-wrap: anywhere; word-break: break-all; min-width: 0;" }, renderFlagEmojis(outbound.displayName) ), E( "span", { style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;" }, [outbound.type].filter(Boolean) ), E( "div", { class: getLatencyClass(), style: "white-space: nowrap; flex-shrink: 0;" }, connectionStatusText ) ] ), E( "button", { type: "button", class: "btn dashboard-sections-grid-item-test-latency", style: "padding: 4px 12px; height: 30px; flex-shrink: 0;", disabled: latencyFetching ? true : void 0, click: (event) => { event.preventDefault(); event.stopPropagation(); if (latencyFetching) return; testLatency(); } }, latencyFetching ? [ renderLoaderCircleIcon24(), E( "span", { class: "dashboard-sections-grid-item-test-latency__label" }, _("Checking...") ) ] : E( "span", { class: "dashboard-sections-grid-item-test-latency__label" }, _("Check Connection") ) ) ] ); } const isManualUrlTest = Boolean(outbound.urlTestInfo?.isManualSelection); const isManualPriority = Boolean(outbound.priorityInfo?.isManualSelection); const activeServerName = outbound.urlTestInfo?.selectedName || outbound.priorityInfo?.selectedName || ""; const typeLabel = isManualUrlTest || isManualPriority ? `${outbound.type} (${_("Manual")})` : outbound.urlTestInfo || outbound.priorityInfo ? `${outbound.type} (${_("Auto")})` : outbound.type; return E( "div", { class: className, "aria-busy": outboundSwitching ? "true" : void 0, "aria-disabled": section.withTagSelect && !canChooseOutbound ? "true" : void 0, click: () => canChooseOutbound && onChooseOutbound(section.sectionName, section.code, outbound.code) }, [ ...outboundSwitching ? [ svgEl( "svg", { class: "tachyon_dashboard-page__outbound-grid__item__snake" }, [ svgEl("rect", { width: "100%", height: "100%", fill: "none", rx: 4, ry: 4, pathLength: 100 }) ] ) ] : [], E( "div", { class: "tachyon_dashboard-page__outbound-grid__item__header" }, [ E("b", {}, renderFlagEmojis(outbound.displayName)), ...canCopyLink ? [ E( "button", { type: "button", class: "btn tachyon_dashboard-page__outbound-grid__item__copy-button", title: _("Copy proxy link"), "aria-label": _("Copy proxy link"), click: (event) => { event.stopPropagation(); onCopyOutbound(section, outbound); } }, renderCopyIcon24() ) ] : [], ...outbound.urlTestInfo ? [ E( "button", { type: "button", class: "btn tachyon_dashboard-page__outbound-grid__item__copy-button", title: _("URLTest details"), "aria-label": _("URLTest details"), click: (event) => { event.stopPropagation(); onShowUrlTestInfo(section, outbound); } }, renderInfoIcon24() ) ] : [], ...outbound.priorityInfo ? [ E( "button", { type: "button", class: "btn tachyon_dashboard-page__outbound-grid__item__copy-button", title: _("Priority details"), "aria-label": _("Priority details"), click: (event) => { event.stopPropagation(); onShowPriorityInfo(section, outbound); } }, renderInfoIcon24() ) ] : [] ] ), ...activeServerName ? [ E( "div", { class: "tachyon_dashboard-page__outbound-grid__item__active-server", style: "font-size: 13px; font-weight: 500; opacity: 0.95; margin: 4px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 4px;", title: activeServerName }, renderFlagEmojis(activeServerName) ) ] : [], E( "div", { class: "tachyon_dashboard-page__outbound-grid__item__footer" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-grid__item__type" }, typeLabel ), E( "div", { class: getLatencyClass() }, isConnectionNode ? connectionStatusText : outbound.latency ? `${outbound.latency}ms` : "N/A" ) ] ) ] ); } if (isConnectionNode) { return E( "div", { class: "tachyon_dashboard-page__outbound-section", style: "border: none; padding: 0;" }, [ E( "div", { style: "display: flex; flex-direction: column; gap: 8px; padding: 0;" }, [...section.outbounds.map((outbound) => renderOutbound(outbound))] ) ] ); } if (isServiceNode && section.serviceStatus) { const ss = section.serviceStatus; const statusColor = ss.ready ? "var(--success-color-medium, green)" : ss.conflict ? "var(--error-color-medium, red)" : ss.configured ? "var(--warn-color-medium, orange)" : "var(--primary-color-low, lightgray)"; const statusText = ss.ready ? _("Running") : ss.conflict ? _("Conflict") : ss.configured ? _("Stopped") : _("Not configured"); const typeLabel = ss.serviceType === "zapret" ? "Zapret" : ss.serviceType === "zapret2" ? "Zapret2" : "ByeDPI"; return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section", style: "cursor: default;" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [ E("span", {}, section.displayName), E( "span", { style: "font-size: 12px; opacity: 0.6; font-weight: normal;" }, typeLabel ) ] ) ] ), E( "div", { style: "display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;" }, [ E("div", { style: "display: flex; align-items: center; gap: 6px;" }, [ E( "span", { style: "opacity: 0.7; font-size: 13px;" }, _("Status") + ":" ), E( "span", { style: `font-size: 13px; font-weight: 500; color: ${statusColor};` }, statusText ) ]), ss.restartCount > 0 ? E( "div", { style: "display: flex; align-items: center; gap: 6px;" }, [ E( "span", { style: "opacity: 0.7; font-size: 13px;" }, _("Restarts") + ":" ), E( "span", { style: "font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);" }, `${ss.restartCount}` ) ] ) : "", ss.unstable ? E( "div", { style: "display: flex; align-items: center; gap: 6px;" }, [ E( "span", { style: "font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);" }, _("Unstable") ) ] ) : "" ] ), ss.statusMessage ? E( "div", { style: "padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;" }, ss.statusMessage ) : "" ]); } const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean); const subscriptionUpdateAction = renderSubscriptionUpdateAction( section, subscriptionUpdating, onUpdateSubscription ); return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [ // Title with test latency E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section", click: (e) => { if (e.target && e.target.closest("button")) return; onToggleCollapse?.(); }, style: "cursor: pointer; user-select: none;" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [ svgEl( "svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})` }, [svgEl("polyline", { points: "6 9 12 15 18 9" })] ), E("span", {}, section.displayName), isCollapsed ? (() => { const selectedOutbound = section.outbounds.find( (o) => o.selected ); if (!selectedOutbound) return ""; const isConnectionNode2 = ["vpn", "awg", "warp"].includes( section.action || "" ); function getLatencyColor() { if (isConnectionNode2) { if (latencyFetching) return "var(--warn-color-medium, orange)"; if (selectedOutbound.latency === -1) return "var(--error-color-medium, red)"; return selectedOutbound.runtimeAvailable ? "var(--success-color-medium, green)" : "var(--error-color-medium, red)"; } if (!selectedOutbound.latency) return "var(--primary-color-low, lightgray)"; if (selectedOutbound.latency < 800) return "var(--success-color-medium, green)"; if (selectedOutbound.latency < 1500) return "var(--warn-color-medium, orange)"; return "var(--error-color-medium, red)"; } let latencyText = ""; if (isConnectionNode2) { latencyText = latencyFetching ? _("Checking...") : selectedOutbound.latency === -1 || !selectedOutbound.runtimeAvailable ? _("Not connected") : selectedOutbound.latency && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : _("Connected"); } else { latencyText = selectedOutbound.latency ? `${selectedOutbound.latency}ms` : ""; } return E( "span", { style: "font-size: 13px; font-weight: normal; margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;" }, [ E( "span", { style: "opacity: 0.7;" }, selectedOutbound.displayName ), latencyText ? E( "span", { style: `color: ${getLatencyColor()};` }, latencyText ) : "" ] ); })() : "" ] ), E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section__actions" }, [ ...subscriptionUpdateAction ? [subscriptionUpdateAction] : [], E( "button", { type: "button", class: "btn dashboard-sections-grid-item-test-latency", "data-latency-section": section.sectionName, disabled: latencyFetching ? true : void 0, click: (event) => { event.preventDefault(); event.stopPropagation(); if (latencyFetching) { return; } testLatency(); } }, latencyFetching ? [ renderLoaderCircleIcon24(), E( "span", { class: "dashboard-sections-grid-item-test-latency__label" }, isConnectionNode ? _("Checking...") : getLatencyTestLabel(latencyProgress) ) ] : E( "span", { class: "dashboard-sections-grid-item-test-latency__label" }, isConnectionNode ? _("Check Connection") : _("Test latency") ) ) ] ) ] ), !isCollapsed ? E("div", { class: "tachyon_dashboard-page__outbound-grid" }, [ ...metadataNodes, ...section.outbounds.map((outbound) => renderOutbound(outbound)) ]) : "" ]); } function renderSections(props) { if (props.failed) { return renderFailedState(); } if (props.loading) { return renderLoadingState(); } return renderDefaultState(props); } // src/tachyon/tabs/dashboard/partials/renderWidget.ts function renderFailedState2() { return E( "div", { id: "", style: "height: 78px", class: "tachyon_dashboard-page__widgets-section__item centered" }, _("Currently unavailable") ); } function renderLoadingState2() { return E( "div", { id: "", style: "height: 78px", class: "tachyon_dashboard-page__widgets-section__item skeleton" }, "" ); } function renderDefaultState2({ title, items }) { return E("div", { class: "tachyon_dashboard-page__widgets-section__item" }, [ E( "b", { class: "tachyon_dashboard-page__widgets-section__item__title" }, title ), ...items.map( (item) => E( "div", { class: `tachyon_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` }, [ E( "span", { class: "tachyon_dashboard-page__widgets-section__item__row__key" }, `${item.key}: ` ), E( "span", { class: "tachyon_dashboard-page__widgets-section__item__row__value" }, item.value ) ] ) ) ]); } function renderWidget(props) { if (props.loading) { return renderLoadingState2(); } if (props.failed) { return renderFailedState2(); } return renderDefaultState2(props); } // src/tachyon/tabs/dashboard/partials/renderConnections.ts function renderConnections(connections, isCollapsed, onToggleCollapse) { if (connections.length === 0) { return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section", style: "cursor: pointer; user-select: none;", click: onToggleCollapse }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [ svgEl( "svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})` }, [svgEl("polyline", { points: "6 9 12 15 18 9" })] ), _("Active Clients") ] ) ] ), isCollapsed ? "" : E( "div", { class: "tachyon_dashboard-page__outbound-section centered", style: "height: 60px;" }, _("No active clients") ) ]); } const rows = connections.map((c) => { return E( "div", { class: "tachyon_dashboard-page__widgets-section__item__row", style: "padding: 8px 0; border-bottom: 1px solid rgba(128, 128, 128, 0.1); display: flex; justify-content: space-between;" }, [ E("div", {}, [ E("b", {}, c.name ? `${c.name} (${c.ip})` : c.ip), E( "span", { style: "opacity: 0.7; font-size: 13px; margin-left: 8px;" }, `(${c.count} conns)` ) ]), E( "div", { style: "font-size: 13px;" }, `\u25B2 ${prettyBytes(c.upload)} | \u25BC ${prettyBytes(c.download)}` ) ] ); }); return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section", style: "cursor: pointer; user-select: none;", click: onToggleCollapse }, [ E( "div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [ svgEl( "svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})` }, [svgEl("polyline", { points: "6 9 12 15 18 9" })] ), _("Active Clients") ] ) ] ), isCollapsed ? "" : E( "div", { class: "tachyon_dashboard-page__outbound-grid", style: "padding: 12px; display: block;" }, rows ) ]); } // src/tachyon/tabs/dashboard/render.ts function render() { return E( "div", { id: "dashboard-status", class: "tachyon_dashboard-page" }, [ E( "div", { class: "tachyon_dashboard-page__service-stopped", role: "status" }, _( "Tachyon service is stopped. Start the service to display the dashboard." ) ), E("div", { class: "tachyon_dashboard-page__content" }, [ // Widgets section E("div", { class: "tachyon_dashboard-page__widgets-section" }, [ E( "div", { id: "dashboard-widget-traffic" }, renderWidget({ loading: true, failed: false, title: "", items: [] }) ), E( "div", { id: "dashboard-widget-traffic-total" }, renderWidget({ loading: true, failed: false, title: "", items: [] }) ), E( "div", { id: "dashboard-widget-system-info" }, renderWidget({ loading: true, failed: false, title: "", items: [] }) ), E( "div", { id: "dashboard-widget-service-info" }, renderWidget({ loading: true, failed: false, title: "", items: [] }) ) ]), // Clients section E("div", { id: "dashboard-connections-grid" }, []), // All outbounds E( "div", { id: "dashboard-sections-grid" }, renderSections({ loading: true, failed: false, section: { code: "", sectionName: "", displayName: "", outbounds: [], withTagSelect: false }, onTestLatency: () => { }, onChooseOutbound: () => { }, onCopyOutbound: () => { }, onShowUrlTestInfo: () => { }, onShowPriorityInfo: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, latencyProgress: void 0, subscriptionUpdating: false, selectorSwitchingTag: void 0 }) ) ]) ] ); } // src/helpers/showToast.ts function showToast(message, type, duration = 3e3) { let container = document.querySelector(".toast-container"); if (!container) { container = document.createElement("div"); container.className = "toast-container"; document.body.appendChild(container); } const toast = document.createElement("div"); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => toast.classList.add("visible"), 100); setTimeout(() => { toast.classList.remove("visible"); setTimeout(() => toast.remove(), 300); }, duration); } // src/helpers/copyToClipboard.ts function copyToClipboard(text) { const textarea = document.createElement("textarea"); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); showToast(_("Copied"), "success"); } catch (_err) { showToast(_("Failed to copy!"), "error"); console.error("copyToClipboard - e", _err); } document.body.removeChild(textarea); } // src/tachyon/methods/custom/getConfigSections.ts async function getConfigSections() { return uci.load(TACHYON_UCI_PACKAGE).then(() => uci.sections(TACHYON_UCI_PACKAGE)); } // src/tachyon/runtimeTags.ts var RESERVED_RUNTIME_TAGS = /* @__PURE__ */ new Set([ "dns-server", "fakeip-server", "bootstrap-dns-server", "fakeip-dns-rule-tag", "fakeip-ruleset-dns-rule-tag", "service-fakeip-dns-rule-tag", "tproxy-in", "tproxy6-in", "dns-in", "service-mixed-in", "direct-out", "bypass-out" ]); function allocateRuntimeTag(base, postfix) { let suffix = 1; let candidate = `${base}-${postfix}`; while (RESERVED_RUNTIME_TAGS.has(candidate)) { candidate = `${base}-${postfix}-${suffix}`; suffix += 1; } return candidate; } function getOutboundTagBySection(sectionName) { return allocateRuntimeTag(sectionName, "out"); } // src/tachyon/methods/shell/callBaseMethod.ts async function callBaseMethod(method, args = [], command = "/usr/bin/tachyon", options = {}) { try { const response = await executeShellCommand({ command, args: [method, ...args], timeout: options.timeout ?? 15e3 }); const exitCode = response.code ?? 0; if (exitCode !== 0 && !(options.allowNonZeroWithStdout && response.stdout)) { return { success: false, error: response.stderr || response.stdout || "" }; } if (response.stdout) { try { return { success: true, data: JSON.parse(response.stdout) }; } catch (_e) { return { success: true, data: response.stdout }; } } return { success: false, error: response.stderr || "" }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "" }; } } // src/tachyon/types.ts var Tachyon; ((Tachyon2) => { let AvailableMethods; ((AvailableMethods2) => { AvailableMethods2["CHECK_DNS_AVAILABLE"] = "check_dns_available"; AvailableMethods2["CHECK_FAKEIP"] = "check_fakeip"; AvailableMethods2["CHECK_NFT_RULES"] = "check_nft_rules"; AvailableMethods2["CHECK_ZAPRET_RUNTIME"] = "check_zapret_runtime"; AvailableMethods2["CHECK_ZAPRET2_RUNTIME"] = "check_zapret2_runtime"; AvailableMethods2["CHECK_BYEDPI_RUNTIME"] = "check_byedpi_runtime"; AvailableMethods2["CHECK_INBOUNDS_CONFIG"] = "check_inbounds_config"; AvailableMethods2["GET_STATUS"] = "get_status"; AvailableMethods2["GET_OUTBOUND_METADATA"] = "get_outbound_metadata"; AvailableMethods2["GET_SUBSCRIPTION_METADATA"] = "get_subscription_metadata"; AvailableMethods2["CHECK_SING_BOX"] = "check_sing_box"; AvailableMethods2["CHECK_INBOUNDS"] = "check_inbounds"; AvailableMethods2["GET_SING_BOX_STATUS"] = "get_sing_box_status"; AvailableMethods2["GET_ZAPRET_STATUS"] = "get_zapret_status"; AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_status"; AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status"; AvailableMethods2["CLASH_API"] = "clash_api"; AvailableMethods2["ENABLE"] = "enable"; AvailableMethods2["DISABLE"] = "disable"; AvailableMethods2["GLOBAL_CHECK"] = "global_check"; AvailableMethods2["DOCTOR"] = "doctor"; AvailableMethods2["AI_DOCTOR"] = "ai_doctor"; AvailableMethods2["AI_DOCTOR_LAST"] = "ai_doctor_last"; AvailableMethods2["APPLY_QUICK_FIX"] = "apply_quick_fix"; AvailableMethods2["LAN_CLIENTS"] = "lan_clients"; AvailableMethods2["TOGGLE_CLIENT_BYPASS"] = "toggle_client_bypass"; AvailableMethods2["SHOW_SING_BOX_CONFIG"] = "show_sing_box_config"; AvailableMethods2["CHECK_LOGS"] = "check_logs"; AvailableMethods2["CHECK_SING_BOX_LOGS"] = "check_sing_box_logs"; AvailableMethods2["GET_SYSTEM_INFO"] = "get_system_info"; AvailableMethods2["GET_SERVER_CAPABILITIES"] = "get_server_capabilities"; AvailableMethods2["GET_UI_CAPABILITIES"] = "get_ui_capabilities"; AvailableMethods2["GET_UI_STATE"] = "get_ui_state"; AvailableMethods2["SERVICE_ACTION_ASYNC"] = "service_action_async"; AvailableMethods2["SERVICE_ACTION_STATUS"] = "service_action_status"; AvailableMethods2["LATENCY_TEST_ASYNC"] = "latency_test_async"; AvailableMethods2["LATENCY_TEST_STATUS"] = "latency_test_status"; AvailableMethods2["UI_ACTION_ACK"] = "ui_action_ack"; AvailableMethods2["COMPONENT_ACTION_ASYNC"] = "component_action_async"; AvailableMethods2["COMPONENT_ACTION_STATUS"] = "component_action_status"; AvailableMethods2["COMPONENT_ACTION_LOG"] = "component_action_log"; AvailableMethods2["COMPONENT_UPDATE_CHECK_CACHE"] = "component_update_check_cache"; AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async"; AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status"; AvailableMethods2["SERVICE_HEALTH_CHECK"] = "service_health_check"; })(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {})); let AvailableClashAPIMethods; ((AvailableClashAPIMethods2) => { AvailableClashAPIMethods2["GET_PROXIES"] = "get_proxies"; AvailableClashAPIMethods2["GET_CONNECTIONS"] = "get_connections"; AvailableClashAPIMethods2["GET_PROXY_LATENCY"] = "get_proxy_latency"; AvailableClashAPIMethods2["GET_PROXY_LATENCIES"] = "get_proxy_latencies"; AvailableClashAPIMethods2["GET_GROUP_LATENCY"] = "get_group_latency"; AvailableClashAPIMethods2["SET_GROUP_PROXY"] = "set_group_proxy"; AvailableClashAPIMethods2["CLOSE_CONNECTION"] = "close_connection"; AvailableClashAPIMethods2["CLOSE_ALL_CONNECTIONS"] = "close_all_connections"; })(AvailableClashAPIMethods = Tachyon2.AvailableClashAPIMethods || (Tachyon2.AvailableClashAPIMethods = {})); })(Tachyon || (Tachyon = {})); // src/tachyon/helpers/isTransientRpcError.ts var TRANSIENT_RPC_ERROR_PATTERNS = [ "no related rpc reply", "request aborted", "operation was aborted", "failed to execute", "networkerror", "fetch failed", "connection refused", "connection reset", "connection timed out", "timeout", "bad gateway", "service unavailable", "ubus error", "ipc error" ]; function isTransientRpcError(message) { if (!message) { return false; } const normalized = message.toLowerCase(); return TRANSIENT_RPC_ERROR_PATTERNS.some( (pattern) => normalized.includes(pattern) ); } // src/tachyon/methods/shell/index.ts var SUBSCRIPTION_UPDATE_RPC_TIMEOUT_MS = 15e3; var SUBSCRIPTION_UPDATE_POLL_INTERVAL_MS = 1500; var UI_ACTION_RPC_TIMEOUT_MS = 15e3; var UI_ACTION_TRANSIENT_RPC_GRACE_MS = 3e4; var SERVICE_ACTION_TIMEOUT_MS = 2 * 60 * 1e3; var SERVICE_ACTION_POLL_INTERVAL_MS = 1e3; var LATENCY_TEST_TIMEOUT_MS = 30 * 1e3; var LATENCY_TEST_POLL_INTERVAL_MS = 1e3; var COMPONENT_ACTION_RPC_TIMEOUT_MS = 1e4; var COMPONENT_ACTION_POLL_INTERVAL_MS = 1e3; var COMPONENT_ACTION_STATUS_REFRESH_INTERVAL_MS = 2e3; var COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS = 2e3; var COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS = 3e4; var COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS = 2e3; var COMPONENT_ACTION_SELF_UPDATE_HARD_TIMEOUT_MS = 12e4; var COMPONENT_ACTION_STATE_DIR = "/var/run/tachyon/component-actions"; var GET_UI_STATE_RPC_TIMEOUT_MS = 3e3; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function translate(message) { return typeof _ === "function" ? _(message) : message; } function parseJsonObjectOutput(output) { if (!output) { return null; } try { return JSON.parse(output); } catch (_error) { const jsonMatch = output.match(/(\{[\s\S]*\})\s*$/); if (!jsonMatch) { return null; } try { return JSON.parse(jsonMatch[1]); } catch (_jsonError) { return null; } } } function parseComponentActionOutput(output) { return parseJsonObjectOutput(output); } function parseComponentActionResult(response) { return parseComponentActionOutput(response.stdout); } function parseComponentActionStartResult(response) { const parsedResponse = parseComponentActionResult(response); if (!parsedResponse) { return null; } return parsedResponse; } function parseSubscriptionUpdateStartResult(response) { return parseJsonObjectOutput( response.stdout ); } function parseSubscriptionUpdateJobState(response) { return parseJsonObjectOutput( response.stdout ); } function parseUiActionStartResult(response) { return parseJsonObjectOutput(response.stdout); } function parseServiceActionState(response) { return parseJsonObjectOutput(response.stdout); } function parseLatencyActionState(response) { return parseJsonObjectOutput(response.stdout); } function isComponentActionJobId(jobId) { return /^[A-Za-z0-9._-]+$/.test(jobId) && jobId !== "." && jobId !== ".."; } async function readComponentActionState(jobId) { if (!isComponentActionJobId(jobId)) { return null; } try { return parseComponentActionOutput( await fs.read(`${COMPONENT_ACTION_STATE_DIR}/${jobId}.json`) ); } catch (_error) { return null; } } async function readTachyonVersion() { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: ["show_version"], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); if ((response.code ?? 0) !== 0 || !response.stdout) { return ""; } return response.stdout.trim(); } async function isComponentActionStillRunning(jobId, component, action) { const response = await callBaseMethod( Tachyon.AvailableMethods.GET_UI_STATE, [], "/usr/bin/tachyon", { timeout: GET_UI_STATE_RPC_TIMEOUT_MS } ); return response.success && response.data.actions.component.some( (state) => state.job_id === jobId && state.component === component && state.action === action && state.running === true ); } function componentActionFailure(response, parsedResponse) { return { success: false, error: parsedResponse?.message || response.stderr || _("Failed to execute") }; } function uiActionFailure(response, parsedResponse, fallback = _("Failed to execute")) { return { success: false, error: parsedResponse?.message || response.stderr || fallback }; } function createTransientRpcGraceTracker(graceMs) { let failureStartedAt = 0; return { reset() { failureStartedAt = 0; }, shouldContinue(error) { if (!isTransientRpcError(error)) { failureStartedAt = 0; return false; } if (!failureStartedAt) { failureStartedAt = Date.now(); } return Date.now() - failureStartedAt < graceMs; } }; } var TachyonShellMethods = { checkDNSAvailable: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_DNS_AVAILABLE ), checkFakeIP: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_FAKEIP ), checkNftRules: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_NFT_RULES ), checkZapretRuntime: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_ZAPRET_RUNTIME ), checkZapret2Runtime: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_ZAPRET2_RUNTIME ), checkByedpiRuntime: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_BYEDPI_RUNTIME ), checkInboundsConfig: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_INBOUNDS_CONFIG ), getStatus: async () => callBaseMethod(Tachyon.AvailableMethods.GET_STATUS), getOutboundMetadata: async (section) => callBaseMethod( Tachyon.AvailableMethods.GET_OUTBOUND_METADATA, [section] ), getSubscriptionMetadata: async (section) => callBaseMethod(Tachyon.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section]), checkSingBox: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_SING_BOX ), checkInbounds: async () => callBaseMethod( Tachyon.AvailableMethods.CHECK_INBOUNDS ), getSingBoxStatus: async () => callBaseMethod( Tachyon.AvailableMethods.GET_SING_BOX_STATUS ), getZapretStatus: async () => callBaseMethod( Tachyon.AvailableMethods.GET_ZAPRET_STATUS ), getZapret2Status: async () => callBaseMethod( Tachyon.AvailableMethods.GET_ZAPRET2_STATUS ), getByedpiStatus: async () => callBaseMethod( Tachyon.AvailableMethods.GET_BYEDPI_STATUS ), getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.GET_PROXIES ]), getClashApiConnections: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.GET_CONNECTIONS ]), getClashApiProxyLatency: async (tag, timeout = "5000") => callBaseMethod( Tachyon.AvailableMethods.CLASH_API, [Tachyon.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, timeout] ), getClashApiProxyLatencies: async (tags) => callBaseMethod( Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.GET_PROXY_LATENCIES, JSON.stringify(tags), "5000" ] ), getClashApiGroupLatency: async (tag) => callBaseMethod( Tachyon.AvailableMethods.CLASH_API, [Tachyon.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"] ), setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.SET_GROUP_PROXY, group, proxy ]), closeClashApiConnection: async (connectionId) => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.CLOSE_CONNECTION, connectionId ]), closeAllClashApiConnections: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.CLOSE_ALL_CONNECTIONS ]), enable: async () => callBaseMethod( Tachyon.AvailableMethods.ENABLE, [], "/etc/init.d/tachyon" ), disable: async () => callBaseMethod( Tachyon.AvailableMethods.DISABLE, [], "/etc/init.d/tachyon" ), globalCheck: async (masked = true) => callBaseMethod(Tachyon.AvailableMethods.GLOBAL_CHECK, [ masked ? "masked" : "raw" ]), doctor: async () => callBaseMethod( Tachyon.AvailableMethods.DOCTOR, [], "/usr/bin/tachyon", { timeout: 3e4 } ), aiDoctor: async () => callBaseMethod( Tachyon.AvailableMethods.AI_DOCTOR, [], "/usr/bin/tachyon", { timeout: 6e4 } ), aiDoctorLast: async () => callBaseMethod( Tachyon.AvailableMethods.AI_DOCTOR_LAST, [], "/usr/bin/tachyon", { timeout: 1e4 } ), applyQuickFix: async (fixCode) => callBaseMethod( Tachyon.AvailableMethods.APPLY_QUICK_FIX, [fixCode], "/usr/bin/tachyon", { timeout: 3e4 } ), getLanClients: async () => callBaseMethod(Tachyon.AvailableMethods.LAN_CLIENTS, [], "/usr/bin/tachyon", { timeout: 1e4 }), toggleClientBypass: async (ip) => callBaseMethod( Tachyon.AvailableMethods.TOGGLE_CLIENT_BYPASS, [ip], "/usr/bin/tachyon", { timeout: 15e3 } ), showSingBoxConfig: async (masked = true) => callBaseMethod(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [ masked ? "masked" : "raw" ]), checkLogs: async () => callBaseMethod(Tachyon.AvailableMethods.CHECK_LOGS), checkSingBoxLogs: async () => callBaseMethod(Tachyon.AvailableMethods.CHECK_SING_BOX_LOGS), getSystemInfo: async () => callBaseMethod( Tachyon.AvailableMethods.GET_SYSTEM_INFO ), getServerCapabilities: async () => callBaseMethod( Tachyon.AvailableMethods.GET_SERVER_CAPABILITIES ), getUiCapabilities: async () => callBaseMethod( Tachyon.AvailableMethods.GET_UI_CAPABILITIES ), getUiState: async () => callBaseMethod( Tachyon.AvailableMethods.GET_UI_STATE, [], "/usr/bin/tachyon", { timeout: GET_UI_STATE_RPC_TIMEOUT_MS } ), serviceActionStart: async (action) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.SERVICE_ACTION_ASYNC, action], timeout: UI_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseUiActionStartResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse?.success || !parsedResponse.job_id) { return uiActionFailure( response, parsedResponse, _("Service action failed") ); } return { success: true, data: parsedResponse }; }, serviceActionStatus: async (jobId) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.SERVICE_ACTION_STATUS, jobId], timeout: UI_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseServiceActionState(response); if ((response.code ?? 0) !== 0 || !parsedResponse) { return uiActionFailure( response, parsedResponse, _("Service action failed") ); } return { success: true, data: parsedResponse }; }, waitServiceActionJob: async (jobId, startedAt = Date.now()) => { while (Date.now() - startedAt < SERVICE_ACTION_TIMEOUT_MS) { await sleep(SERVICE_ACTION_POLL_INTERVAL_MS); const response = await TachyonShellMethods.serviceActionStatus(jobId); if (!response.success) { return response; } if (response.data.running) { continue; } return response; } return { success: false, error: _("Operation timed out") }; }, latencyTestStart: async (latencyType, section, tag, timeout) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [ Tachyon.AvailableMethods.LATENCY_TEST_ASYNC, latencyType, section, tag, ...timeout ? [timeout] : [] ], timeout: UI_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseUiActionStartResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse?.success || !parsedResponse.job_id) { return uiActionFailure( response, parsedResponse, _("Latency test failed") ); } return { success: true, data: parsedResponse }; }, latencyTestStatus: async (jobId) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.LATENCY_TEST_STATUS, jobId], timeout: UI_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseLatencyActionState(response); if ((response.code ?? 0) !== 0 || !parsedResponse) { return uiActionFailure( response, parsedResponse, _("Latency test failed") ); } return { success: true, data: parsedResponse }; }, waitLatencyTestJob: async (jobId, startedAt = Date.now()) => { const transientRpc = createTransientRpcGraceTracker( UI_ACTION_TRANSIENT_RPC_GRACE_MS ); while (Date.now() - startedAt < LATENCY_TEST_TIMEOUT_MS) { await sleep(LATENCY_TEST_POLL_INTERVAL_MS); const response = await TachyonShellMethods.latencyTestStatus(jobId); if (!response.success) { if (transientRpc.shouldContinue(response.error)) { continue; } return response; } transientRpc.reset(); if (response.data.running) { continue; } return response; } return { success: false, error: _("Operation timed out") }; }, uiActionAck: async (kind, jobId) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.UI_ACTION_ACK, kind, jobId], timeout: UI_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseUiActionStartResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse?.success) { return uiActionFailure(response, parsedResponse); } return { success: true, data: parsedResponse }; }, componentActionStart: async (component, action) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [ Tachyon.AvailableMethods.COMPONENT_ACTION_ASYNC, component, action ], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseComponentActionStartResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse?.success || !parsedResponse.job_id) { return componentActionFailure(response, parsedResponse); } return { success: true, data: parsedResponse }; }, componentActionStatus: async (jobId) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.COMPONENT_ACTION_STATUS, jobId], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseComponentActionResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse) { return componentActionFailure(response, parsedResponse); } return { success: true, data: parsedResponse }; }, componentActionLog: async (jobId, offset = 0) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [ Tachyon.AvailableMethods.COMPONENT_ACTION_LOG, jobId, String(Math.max(0, Math.floor(offset))) ], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseJsonObjectOutput(response.stdout); if ((response.code ?? 0) !== 0 || !parsedResponse) { return { success: false, error: parsedResponse?.success === false ? _("Operation log is not available") : response.stderr || _("Failed to read operation log") }; } return { success: true, data: parsedResponse }; }, componentUpdateCheckCache: async () => callBaseMethod( Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE ), waitComponentActionJob: async (jobId, component, action, expectedLatestVersion) => { const jobStartedAt = Date.now(); const isSelfUpdate = component === "tachyon" && (action === "install" || action === "reinstall"); const targetVersion = expectedLatestVersion || ""; let baselineVersion = ""; if (isSelfUpdate) { baselineVersion = await readTachyonVersion(); } let selfUpdateVersionMatchedAt = 0; let lastStatusRefreshAt = 0; const transientRpc = createTransientRpcGraceTracker( COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS ); const versionsMatch = (a, b) => { const cleanA = a.replace(/^v/i, "").trim(); const cleanB = b.replace(/^v/i, "").trim(); return cleanA === cleanB; }; const confirmedByVersion = async () => { if (!isSelfUpdate) return ""; if (Date.now() - jobStartedAt < COMPONENT_ACTION_MIN_ELAPSED_FOR_SELF_UPDATE_MS) { return ""; } const version = await readTachyonVersion(); if (!version) return ""; if (targetVersion && versionsMatch(version, targetVersion)) { return version; } if (!targetVersion && baselineVersion && !versionsMatch(version, baselineVersion)) { return version; } return ""; }; const confirmedSameVersionReinstall = async () => { if (!isSelfUpdate || action !== "reinstall" || !baselineVersion) { return ""; } const version = await readTachyonVersion(); if (versionsMatch(version, baselineVersion) && (!targetVersion || versionsMatch(version, targetVersion))) { return version; } return ""; }; const settleVersion = async (version) => { if (!selfUpdateVersionMatchedAt) { selfUpdateVersionMatchedAt = Date.now(); return false; } if (Date.now() - selfUpdateVersionMatchedAt < COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS) { return false; } if (await confirmedByVersion() === version) { return true; } if (version === baselineVersion && await confirmedSameVersionReinstall() === version) { return true; } selfUpdateVersionMatchedAt = 0; return false; }; const selfUpdateResult = (installedVersion) => ({ success: true, data: { success: true, component, action, message: translate("Tachyon has been installed"), current_version: installedVersion, latest_version: expectedLatestVersion, changed: true, status: "latest" } }); const jobDoneResult = (data) => ({ success: true, data }); while (true) { await sleep(COMPONENT_ACTION_POLL_INTERVAL_MS); const stateResponse = await readComponentActionState(jobId); if (isSelfUpdate && Date.now() - jobStartedAt >= COMPONENT_ACTION_SELF_UPDATE_HARD_TIMEOUT_MS) { if (stateResponse && !stateResponse.running) { return jobDoneResult(stateResponse); } const version = await readTachyonVersion(); if (targetVersion && version && version !== targetVersion) { return { success: false, error: _("Tachyon update did not complete within the timeout") }; } return selfUpdateResult(version || baselineVersion); } if (stateResponse && !stateResponse.running) { if (isSelfUpdate && stateResponse.success === false) { const version = await confirmedByVersion() || await confirmedSameVersionReinstall(); if (version) { if (await settleVersion(version)) { return selfUpdateResult(version); } continue; } } return jobDoneResult(stateResponse); } if (stateResponse && stateResponse.running && isSelfUpdate && Date.now() - lastStatusRefreshAt >= COMPONENT_ACTION_STATUS_REFRESH_INTERVAL_MS) { const version = await confirmedByVersion(); if (version && await settleVersion(version)) { return selfUpdateResult(version); } } lastStatusRefreshAt = Date.now(); const statusResponse = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.COMPONENT_ACTION_STATUS, jobId], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseComponentActionResult(statusResponse); if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) { if (stateResponse?.running) { transientRpc.reset(); continue; } if (isSelfUpdate) { const version = await confirmedByVersion() || await confirmedSameVersionReinstall(); if (version) { if (await settleVersion(version)) { return selfUpdateResult(version); } continue; } continue; } if (await isComponentActionStillRunning(jobId, component, action)) { transientRpc.reset(); continue; } const failure = componentActionFailure(statusResponse, parsedResponse); if (transientRpc.shouldContinue(failure.error)) { continue; } return failure; } transientRpc.reset(); if (parsedResponse.running) { if (isSelfUpdate) { const version = await confirmedByVersion(); if (version && await settleVersion(version)) { return selfUpdateResult(version); } } continue; } if (isSelfUpdate && parsedResponse.success === false) { const version = await confirmedByVersion() || await confirmedSameVersionReinstall(); if (version) { if (await settleVersion(version)) { return selfUpdateResult(version); } continue; } } return jobDoneResult(parsedResponse); } }, subscriptionUpdateStart: async (section, sourceIndex) => { const startArgs = [ Tachyon.AvailableMethods.SUBSCRIPTION_UPDATE_ASYNC, ...section ? [section] : [], ...section && sourceIndex !== void 0 ? [String(sourceIndex)] : [] ]; const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: startArgs, timeout: SUBSCRIPTION_UPDATE_RPC_TIMEOUT_MS }); const parsedResponse = parseSubscriptionUpdateStartResult(response); if ((response.code ?? 0) !== 0 || !parsedResponse?.success || !parsedResponse.job_id) { return { success: false, error: parsedResponse?.message || response.stderr || _("Subscription update failed") }; } return { success: true, data: parsedResponse }; }, subscriptionUpdateStatus: async (jobId) => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: [Tachyon.AvailableMethods.SUBSCRIPTION_UPDATE_STATUS, jobId], timeout: SUBSCRIPTION_UPDATE_RPC_TIMEOUT_MS }); const parsedResponse = parseSubscriptionUpdateJobState(response); if ((response.code ?? 0) !== 0 || !parsedResponse) { return { success: false, error: response.stderr || _("Subscription update failed") }; } return { success: true, data: parsedResponse }; }, waitSubscriptionUpdateJob: async (jobId) => { const transientRpc = createTransientRpcGraceTracker( UI_ACTION_TRANSIENT_RPC_GRACE_MS ); while (true) { await sleep(SUBSCRIPTION_UPDATE_POLL_INTERVAL_MS); const response = await TachyonShellMethods.subscriptionUpdateStatus(jobId); if (!response.success) { if (transientRpc.shouldContinue(response.error)) { continue; } return response; } transientRpc.reset(); if (response.data.running) { continue; } return response; } }, getWatchdogStatus: async () => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: ["watchdog", "status"], timeout: 5e3 }); return { success: true, data: { running: (response.code ?? 1) === 0 } }; }, watchdogStart: async () => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: ["watchdog_start"], timeout: 8e3 }); return { success: (response.code ?? 1) === 0 }; }, watchdogStop: async () => { const response = await executeShellCommand({ command: "/usr/bin/tachyon", args: ["watchdog_stop"], timeout: 8e3 }); return { success: (response.code ?? 1) === 0 }; }, /** * Run an arbitrary UCI command via shell — used for saving Smart Detect * settings and per-device routing IPs from the Advanced Settings panel. */ uciRunCommand: async (args) => { const response = await executeShellCommand({ command: "/sbin/uci", args, timeout: 5e3 }); return { success: (response.code ?? 1) === 0 }; } }; // src/tachyon/methods/custom/getDashboardSections.ts var DASHBOARD_SECTION_CACHE_DIR = "/var/run/tachyon/section-cache"; var CLASH_API_FETCH_TIMEOUT_MS = 5e3; function getDisplayName(section) { return section.label || section[".name"]; } function getSettingsSection(configSections) { return configSections.find((section) => section[".type"] === "settings"); } function getClashApiSecret(configSections) { return getSettingsSection(configSections)?.yacd_secret_key || ""; } function canFetchClashApiDirectly() { return canUseDirectClashApi() && typeof fetch === "function"; } async function getClashApiProxies(configSections) { if (canFetchClashApiDirectly()) { const secret = getClashApiSecret(configSections); const controller = new AbortController(); const timeoutId = setTimeout( () => controller.abort(), CLASH_API_FETCH_TIMEOUT_MS ); try { const response = await fetch(`${getClashHttpUrl()}/proxies`, { headers: secret ? { Authorization: `Bearer ${secret}` } : void 0, signal: controller.signal }); if (response.ok) { return { success: true, data: await response.json() }; } } catch (_error) { } finally { clearTimeout(timeoutId); } } return TachyonShellMethods.getClashApiProxies(); } function getListValues(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map((item) => `${item}`.trim()).filter(Boolean); } return `${value}`.split(/\s+/).map((item) => item.trim()).filter(Boolean); } function childSections(configSections, type) { return configSections.filter((section) => section[".type"] === type); } function ownedChildSections(parent, children) { return children.filter( (section) => section.section === parent[".name"] ); } function childSectionsByOwner(children, ownerKey, ownerValue) { return children.filter((section) => section[ownerKey] === ownerValue); } function compactSettingsMap(settings) { return Object.keys(settings).length ? JSON.stringify(settings) : void 0; } function hydrateConfigSections(configSections) { const subscriptionUrls = childSections(configSections, "subscription_url"); const interfaces = childSections(configSections, "section_interface"); const urltests = childSections(configSections, "urltest"); const priorityGroups = childSections(configSections, "priority_group"); const priorityLevels = childSections(configSections, "priority_level"); return configSections.map((section) => { if (section[".type"] !== "section") { return section; } const next = { ...section }; const subscriptionUrlItems = ownedChildSections(next, subscriptionUrls); const interfaceItems = ownedChildSections(next, interfaces); const urltestItems = ownedChildSections(next, urltests); const priorityGroupItems = ownedChildSections(next, priorityGroups); if (subscriptionUrlItems.length) { const settings = {}; next.subscription_urls = subscriptionUrlItems.map((item) => item.url || "").filter(Boolean); subscriptionUrlItems.forEach((item) => { if (!item.url) { return; } settings[item.url] = { subscription_update_enabled: item.subscription_update_enabled, subscription_update_interval: item.subscription_update_interval, download_via_proxy_enabled: item.download_via_proxy_enabled, download_via_proxy_section: item.download_via_proxy_section, auto_user_agent: item.auto_user_agent, user_agent: item.user_agent, auto_hwid: item.auto_hwid, hwid: item.hwid, show_dashboard_metadata: item.show_dashboard_metadata, prefix_nodes: item.prefix_nodes, node_prefix: item.node_prefix, include_urltest_groups: item.include_urltest_groups, hide_urltest_group_outbounds: item.hide_urltest_group_outbounds, hide_detour_outbounds: item.hide_detour_outbounds }; }); next.subscription_url_settings = compactSettingsMap(settings); } if (interfaceItems.length) { next.interfaces = interfaceItems.map((item) => item.name || "").filter(Boolean); } if (urltestItems.length) { const settings = {}; next.urltests = urltestItems.map((item) => item[".name"]); urltestItems.forEach((item) => { settings[item[".name"]] = { display_name: item.name || item.display_name, urltest_check_interval: item.check_interval, urltest_tolerance: item.tolerance, urltest_testing_url: item.testing_url, idle_timeout: item.idle_timeout, interrupt_exist_connections: item.interrupt_exist_connections, pin_dashboard: item.pin_dashboard, urltest_filter_mode: item.filter_mode, detect_server_country: item.detect_server_country, urltest_include_countries: item.include_countries, urltest_include_outbounds: item.include_outbounds, urltest_include_regex: item.include_regex, urltest_exclude_countries: item.exclude_countries, urltest_exclude_outbounds: item.exclude_outbounds, urltest_exclude_regex: item.exclude_regex }; }); next.urltest_settings = compactSettingsMap(settings); } if (priorityGroupItems.length) { const settings = {}; next.priority_groups = priorityGroupItems.map((item) => item[".name"]); priorityGroupItems.forEach((item) => { const groupId = item[".name"]; const levels = childSectionsByOwner(priorityLevels, "group", groupId).map( (level, index) => ({ id: level[".name"], displayName: level.name || level[".name"], order: Number.parseInt(level.order || `${index}`, 10) || 0, direct: level.direct === "1", filterMode: level.filter_mode || "include", detectServerCountry: level.detect_server_country || "flag_emoji", country: getListValues(level.country), serverName: getListValues(level.server_name), regex: getListValues(level.regex), excludeCountries: getListValues(level.exclude_countries), excludeOutbounds: getListValues(level.exclude_outbounds), excludeRegex: getListValues(level.exclude_regex) }) ).sort( (left, right) => left.order === right.order ? left.id.localeCompare(right.id) : left.order - right.order ); settings[groupId] = { name: item.name, health_url: item.health_url, active_check_interval: item.active_check_interval, check_timeout: item.check_timeout, recovery_check_interval: item.recovery_check_interval, pick_fastest: item.pick_fastest, switch_to_faster_same_priority: item.switch_to_faster_same_priority, fastest_check_interval: item.fastest_check_interval, interrupt_exist_connections: item.interrupt_exist_connections, pin_dashboard: item.pin_dashboard, levels }; }); next.priority_group_settings = compactSettingsMap(settings); } return next; }); } function getManualProxyLinks(section) { return getListValues(section.selector_proxy_links); } function getConnectionInterfaces(section) { const values = getListValues(section.interfaces); return values.length ? values : getListValues(section.interface); } function getJsonOutbounds(section) { const values = getListValues(section.outbound_jsons); return values.length ? values : getListValues(section.outbound_json); } function isConnectionAction(action) { return Boolean( action && ["connection", "proxy", "outbound", "vpn", "awg", "warp"].includes( action ) ); } function isServiceAction(action) { return Boolean(action && ["zapret", "zapret2", "byedpi"].includes(action)); } function hasSubscriptionSources(section) { return getSubscriptionSourceCount(section) > 0; } function getSubscriptionSourceCount(section) { return getListValues(section.subscription_urls).length; } function shouldSortByLatency(section) { return section.sort_by_latency === "1"; } function hasConfiguredUrlTestList(section) { return getListValues(section.urltests).length > 0; } function hasConfiguredPriorityList(section) { return getListValues(section.priority_groups).length > 0; } function getUrlTestIds(section) { const values = getListValues(section.urltests); return values.length ? values : section.urltest_enabled === "1" ? ["urltest"] : []; } function isUrlTestEnabled(section) { return getUrlTestIds(section).length > 0; } function shouldUseProxyGroup(section) { return getManualProxyLinks(section).length > 0 || hasSubscriptionSources(section) || getConnectionInterfaces(section).length > 0 || getJsonOutbounds(section).length > 0 || isUrlTestEnabled(section) || hasConfiguredPriorityList(section); } function getSectionProxyConfigType(section) { if (hasSubscriptionSources(section)) { return "subscription"; } if (isUrlTestEnabled(section) && shouldUseProxyGroup(section)) { return "urltest"; } if (getManualProxyLinks(section).length > 0) { return "selector"; } if (getJsonOutbounds(section).length > 0) { return "outbound"; } if (getConnectionInterfaces(section).length > 0) { return "interface"; } if (hasConfiguredPriorityList(section)) { return "selector"; } return void 0; } function getJsonOutboundDisplayName(section) { try { const parsedOutbound = JSON.parse(section.outbound_json || "{}"); return parsedOutbound?.tag ? decodeURIComponent(parsedOutbound.tag) : ""; } catch (_error) { return ""; } } function buildManualLinkByCode(section) { const sectionName = section[".name"]; return new Map( getManualProxyLinks(section).map((link, index) => [ getOutboundTagBySection(`${sectionName}-${index + 1}`), link ]) ); } function getProxyEntryByCode(proxies) { return new Map(proxies.map((proxy) => [proxy.code, proxy])); } function uniqueCodes(codes) { return Array.from(new Set(codes.filter(Boolean))); } function isSelectorOutbound(outbound) { return outbound.type?.toLowerCase() === "selector"; } function isUrlTestProxyEntry(entry) { return entry?.value?.type?.toLowerCase() === "urltest"; } function getLatencySortValue(outbound) { const latency = Number(outbound.latency); return Number.isFinite(latency) && latency > 0 ? latency : Number.POSITIVE_INFINITY; } function sortOutboundsForDashboard(outbounds, options = {}) { const pinnedCodes = [ ...options.pinnedCode ? [options.pinnedCode] : [], ...options.pinnedCodes || [] ].filter(Boolean); const pinnedRank = new Map( pinnedCodes.map((code, index) => [code, index]) ); const sortByLatency = options.sortByLatency === true; return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => { const leftPinned = pinnedRank.has(left.outbound.code); const rightPinned = pinnedRank.has(right.outbound.code); if (leftPinned !== rightPinned) { return leftPinned ? -1 : 1; } if (leftPinned && rightPinned) { const rankDiff = (pinnedRank.get(left.outbound.code) ?? 0) - (pinnedRank.get(right.outbound.code) ?? 0); if (rankDiff !== 0) { return rankDiff; } } if (sortByLatency) { const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound); if (latencyDiff !== 0) { return latencyDiff; } } return left.index - right.index; }).map((item) => item.outbound); } function sortUrlTestMembers(outbounds) { return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => { if (left.outbound.selected !== right.outbound.selected) { return left.outbound.selected ? -1 : 1; } const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound); if (latencyDiff !== 0) { return latencyDiff; } return left.index - right.index; }).map((item) => item.outbound); } function isSafeSectionName(sectionName) { return /^[A-Za-z0-9_-]+$/.test(sectionName); } function objectMap(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return {}; } return Object.fromEntries( Object.entries(value).filter(([, item]) => typeof item === "string").map(([key, item]) => [key, item]) ); } function itemSettingsMap(value) { if (!value) { return {}; } try { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return Object.fromEntries( Object.entries(parsed).filter( ([, item]) => item && typeof item === "object" && !Array.isArray(item) ) ); } catch (_error) { return {}; } } function itemSettingString(settings, key, fallback = "") { const value = settings?.[key]; return typeof value === "string" ? value : fallback; } function itemSettingBoolean(settings, key, fallback) { const value = settings?.[key]; if (value === void 0 || value === null || value === "") { return fallback; } return value === "1" || value === "true"; } function isUrlTestFilteringEnabled(settings) { return ["exclude", "include", "mixed"].includes( itemSettingString(settings, "urltest_filter_mode", "disabled") ); } function getUrlTestTag(sectionName, id) { return getOutboundTagBySection( id === "urltest" ? `${sectionName}-urltest` : `${sectionName}-urltest-${id}` ); } function getPriorityTag(sectionName, id) { return getOutboundTagBySection(`${sectionName}-priority-${id}`); } function getUrlTestDisplayName(section, id, settings) { return itemSettingString( settings, "display_name", id === "urltest" && !hasConfiguredUrlTestList(section) ? _("Fastest") : id ); } function getUrlTestConfigs(section) { const settingsMap = itemSettingsMap(section.urltest_settings); const sectionName = section[".name"]; return getUrlTestIds(section).map((id) => { const settings = settingsMap[id] || {}; const filteringEnabled = isUrlTestFilteringEnabled(settings); return { id, code: getUrlTestTag(sectionName, id), displayName: getUrlTestDisplayName(section, id, settings), settings, pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true), showDetectedCountries: filteringEnabled && itemSettingString(settings, "detect_server_country", "flag_emoji") === "country_is" }; }); } function priorityLevelConfigsFromSettings(settings) { const value = settings?.levels; if (!Array.isArray(value)) { return []; } return value.flatMap((item, index) => { if (!item || typeof item !== "object" || Array.isArray(item)) { return []; } const level = item; const id = `${level.id || ""}`.trim(); if (!id) { return []; } return [ { id, displayName: `${level.displayName || id}`, order: typeof level.order === "number" && Number.isFinite(level.order) ? level.order : index, direct: Boolean(level.direct), filterMode: `${level.filterMode || "include"}` || "include", detectServerCountry: `${level.detectServerCountry || "flag_emoji"}` || "flag_emoji", country: getListValues(level.country), serverName: getListValues(level.serverName), regex: getListValues(level.regex), excludeCountries: getListValues( level.excludeCountries ), excludeOutbounds: getListValues( level.excludeOutbounds ), excludeRegex: getListValues(level.excludeRegex), outbounds: getListValues(level.outbounds) } ]; }).sort( (left, right) => left.order === right.order ? left.id.localeCompare(right.id) : left.order - right.order ); } function getPriorityGroupIds(section) { return getListValues(section.priority_groups); } function getPriorityConfigs(section) { const settingsMap = itemSettingsMap(section.priority_group_settings); const sectionName = section[".name"]; return getPriorityGroupIds(section).map((id) => { const settings = settingsMap[id] || {}; const levels = priorityLevelConfigsFromSettings(settings); return { id, code: getPriorityTag(sectionName, id), displayName: itemSettingString(settings, "name", id), settings, pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true), healthUrl: itemSettingString( settings, "health_url", "https://www.gstatic.com/generate_204" ), activeCheckInterval: itemSettingString( settings, "active_check_interval", "5s" ), checkTimeout: itemSettingString(settings, "check_timeout", "2s"), recoveryCheckInterval: itemSettingString( settings, "recovery_check_interval", "15s" ), pickFastest: itemSettingBoolean(settings, "pick_fastest", false), switchToFasterSamePriority: itemSettingBoolean( settings, "switch_to_faster_same_priority", false ), fastestCheckInterval: itemSettingString( settings, "fastest_check_interval", "3m" ), interruptExistConnections: itemSettingBoolean( settings, "interrupt_exist_connections", true ), showDetectedCountries: levels.some( (level) => level.detectServerCountry === "country_is" ), levels }; }); } async function readDashboardSectionCache(sectionName) { if (!isSafeSectionName(sectionName)) { return void 0; } try { const raw = await fs.read( `${DASHBOARD_SECTION_CACHE_DIR}/${sectionName}.json` ); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return void 0; } return parsed; } catch (_error) { return void 0; } } function getUrlTestGroups(dashboardCache) { const groups = dashboardCache?.urltestGroups; if (!groups || typeof groups !== "object" || Array.isArray(groups)) { return {}; } return groups; } function getPriorityGroups(dashboardCache) { const groups = dashboardCache?.priorityGroups; if (!groups || typeof groups !== "object" || Array.isArray(groups)) { return {}; } return groups; } function getOutboundDisplayName(code, entry, link, outboundMetadata, preferMetadata = false) { const metadataName = outboundMetadata?.names?.[code]; return (preferMetadata ? metadataName : getProxyUrlName(link)) || (preferMetadata ? getProxyUrlName(link) : metadataName) || entry?.value?.name || code; } function buildUrlTestInfo({ code, displayName, entry, groupCache, proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, showDetectedCountries, selectorNow, selectorCodes = [] }) { const childCodes = uniqueCodes( groupCache?.outbounds?.length ? groupCache.outbounds : entry?.value.all || [] ); const isChildSelectedInSelector = Boolean( selectorNow && selectorNow !== code && childCodes.includes(selectorNow) && !selectorCodes.includes(selectorNow) ); const selectedCode = isChildSelectedInSelector ? selectorNow : entry?.value.now || ""; const isManualSelection = isChildSelectedInSelector; const outbounds = sortUrlTestMembers( childCodes.flatMap((childCode) => { const childEntry = proxyByCode.get(childCode); const link = manualLinkByCode.get(childCode) || cachedProxyLinks.get(childCode) || ""; const canCopyLink = isCopyableProxyLink(link); return [ { code: childCode, displayName: getOutboundDisplayName( childCode, childEntry, link, outboundMetadata, cachedProxyLinks.has(childCode) ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || "", selected: selectedCode === childCode, link, canCopyLink, country: showDetectedCountries ? outboundMetadata?.countries?.[childCode] : void 0 } ]; }) ); const selectedName = outbounds.find((outbound) => outbound.code === selectedCode)?.displayName || selectedCode; return { code, displayName: groupCache?.displayName || displayName, selectedCode: selectedCode || void 0, selectedName: selectedName || void 0, isManualSelection, url: groupCache?.url, interval: groupCache?.interval, tolerance: groupCache?.tolerance, idleTimeout: groupCache?.idle_timeout || "30m", interruptExistConnections: groupCache?.interrupt_exist_connections, outbounds }; } function buildPriorityInfo({ config, entry, groupCache, proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, showDetectedCountries, selectorNow, selectorCodes = [] }) { const cacheLevels = Array.isArray(groupCache?.levels) ? groupCache.levels : []; const configLevelById = new Map( config.levels.map((level) => [level.id, level]) ); const levels = (cacheLevels.length ? cacheLevels.map((level, index) => { const id = `${level.id || ""}`; const configLevel = id ? configLevelById.get(id) : void 0; return { id: id || configLevel?.id || `level-${index + 1}`, displayName: level.displayName || configLevel?.displayName || `${index + 1}`, order: typeof level.order === "number" && Number.isFinite(level.order) ? level.order : configLevel?.order ?? index, direct: level.direct ?? configLevel?.direct ?? false, filterMode: level.filter_mode || configLevel?.filterMode || "include", detectServerCountry: level.detect_server_country || configLevel?.detectServerCountry || "flag_emoji", country: configLevel?.country || [], serverName: configLevel?.serverName || [], regex: configLevel?.regex || [], excludeCountries: configLevel?.excludeCountries || [], excludeOutbounds: configLevel?.excludeOutbounds || [], excludeRegex: configLevel?.excludeRegex || [], outbounds: uniqueCodes(level.outbounds || []) }; }) : config.levels.map((level) => ({ ...level, outbounds: uniqueCodes(level.outbounds || []) }))).sort( (left, right) => left.order === right.order ? left.id.localeCompare(right.id) : left.order - right.order ); const allChildCodes = levels.flatMap((l) => l.outbounds); const isChildSelectedInSelector = Boolean( selectorNow && selectorNow !== config.code && allChildCodes.includes(selectorNow) && !selectorCodes.includes(selectorNow) ); const selectedCode = isChildSelectedInSelector ? selectorNow : entry?.value.now || ""; const isManualSelection = isChildSelectedInSelector; const outbounds = levels.flatMap((level, levelIndex) => { const members = uniqueCodes(level.outbounds || []).map((childCode) => { const childEntry = proxyByCode.get(childCode); const link = manualLinkByCode.get(childCode) || cachedProxyLinks.get(childCode) || ""; const canCopyLink = isCopyableProxyLink(link); return { code: childCode, displayName: getOutboundDisplayName( childCode, childEntry, link, outboundMetadata, cachedProxyLinks.has(childCode) ), latency: childEntry?.value?.history?.[0]?.delay || 0, type: childEntry?.value?.type || "", selected: selectedCode === childCode, link, canCopyLink, country: showDetectedCountries ? outboundMetadata?.countries?.[childCode] : void 0, levelIndex, levelName: level.displayName, levelId: level.id }; }); if (!config.pickFastest) { return members; } return members.map((outbound, index) => ({ outbound, index })).sort((left, right) => { const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound); return latencyDiff !== 0 ? latencyDiff : left.index - right.index; }).map((item) => item.outbound); }); const selectedName = outbounds.find((outbound) => outbound.code === selectedCode)?.displayName || selectedCode; return { code: config.code, displayName: groupCache?.displayName || config.displayName, selectedCode: selectedCode || void 0, selectedName: selectedName || void 0, isManualSelection, healthUrl: groupCache?.health_url || config.healthUrl, activeCheckInterval: groupCache?.active_check_interval || config.activeCheckInterval, checkTimeout: groupCache?.check_timeout || config.checkTimeout, recoveryCheckInterval: groupCache?.recovery_check_interval || config.recoveryCheckInterval, pickFastest: groupCache?.pick_fastest ?? config.pickFastest, switchToFasterSamePriority: groupCache?.switch_to_faster_same_priority ?? config.switchToFasterSamePriority, fastestCheckInterval: groupCache?.fastest_check_interval || config.fastestCheckInterval, interruptExistConnections: groupCache?.interrupt_exist_connections ?? config.interruptExistConnections, outbounds }; } function buildProxyGroupOutbounds(section, proxies, outboundMetadata, urltestGroups = {}, priorityGroups = {}, cachedProxyLinks = /* @__PURE__ */ new Map()) { const sectionName = section[".name"]; const proxyByCode = getProxyEntryByCode(proxies); const selectorTag = getOutboundTagBySection(sectionName); const selector = proxyByCode.get(selectorTag); const urlTestConfigs = getUrlTestConfigs(section); const urlTestConfigByCode = new Map( urlTestConfigs.map((config) => [config.code, config]) ); const priorityConfigs = getPriorityConfigs(section); const priorityConfigByCode = new Map( priorityConfigs.map((config) => [config.code, config]) ); const urlTestEntries = urlTestConfigs.map((config) => ({ config, entry: proxyByCode.get(config.code) })); const priorityEntries = priorityConfigs.map((config) => ({ config, entry: proxyByCode.get(config.code) })); const manualLinkByCode = buildManualLinkByCode(section); const selectorCodes = selector?.value?.all ?? []; const urlTestCodes = urlTestConfigs.map((config) => config.code); const priorityCodes = priorityConfigs.map((config) => config.code); const showDetectedCountries = urlTestConfigs.some((config) => config.showDetectedCountries) || priorityConfigs.some((config) => config.showDetectedCountries); const builtInUrltestCode = urlTestCodes[0] || ""; const fallbackCodes = uniqueCodes([ ...urlTestCodes, ...priorityCodes, ...urlTestEntries.flatMap(({ entry }) => entry?.value.all || []), ...priorityEntries.flatMap(({ entry }) => entry?.value.all || []) ]); const groupCodes = uniqueCodes([ ...selectorCodes.length ? selectorCodes : fallbackCodes, ...urlTestCodes, ...priorityCodes ]); const selectorNow = selector?.value?.now; const outbounds = uniqueCodes(groupCodes).flatMap((code) => { const item = proxyByCode.get(code); const urlTestConfig = urlTestConfigByCode.get(code); const priorityConfig = priorityConfigByCode.get(code); if (!item && !urlTestConfig && !priorityConfig) { return []; } const link = manualLinkByCode.get(code) || cachedProxyLinks.get(code) || ""; const canCopyLink = isCopyableProxyLink(link); const displayName = priorityConfig?.displayName || urlTestConfig?.displayName || getOutboundDisplayName( code, item, link, outboundMetadata, cachedProxyLinks.has(code) ); const isRuntimeUrlTest = isUrlTestProxyEntry(item); const urlTestInfo = urlTestConfig || isRuntimeUrlTest ? buildUrlTestInfo({ code, displayName, entry: item, groupCache: urltestGroups[code], proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, showDetectedCountries: urlTestConfig?.showDetectedCountries || showDetectedCountries, selectorNow, selectorCodes }) : void 0; const priorityInfo = priorityConfig ? buildPriorityInfo({ config: priorityConfig, entry: item, groupCache: priorityGroups[code], proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, showDetectedCountries: priorityConfig.showDetectedCountries, selectorNow, selectorCodes }) : void 0; const isUrlTestChildSelected = Boolean( urlTestInfo?.isManualSelection && urlTestInfo?.selectedCode && urlTestInfo.outbounds.some((m) => m.code === selectorNow) ); const isPriorityChildSelected = Boolean( priorityInfo?.isManualSelection && priorityInfo?.selectedCode && priorityInfo.outbounds.some((m) => m.code === selectorNow) ); const isSelected = selectorNow === code || isUrlTestChildSelected || isPriorityChildSelected; const activeMemberLatency = isUrlTestChildSelected && urlTestInfo?.outbounds.find((m) => m.code === selectorNow)?.latency || isPriorityChildSelected && priorityInfo?.outbounds.find((m) => m.code === selectorNow)?.latency || urlTestInfo?.outbounds.find( (m) => m.selected || m.code === urlTestInfo.selectedCode )?.latency || priorityInfo?.outbounds.find( (m) => m.selected || m.code === priorityInfo.selectedCode )?.latency || 0; const latency = item?.value.history?.[0]?.delay || activeMemberLatency || 0; return [ { code, displayName, latency, type: priorityConfig ? "Priority" : item?.value.type || "URLTest", selected: isSelected, link, canCopyLink, country: showDetectedCountries ? outboundMetadata?.countries?.[code] : void 0, runtimeAvailable: item ? void 0 : false, urlTestInfo, priorityInfo } ]; }); const sortedOutbounds = sortOutboundsForDashboard(outbounds, { pinnedCodes: [ ...urlTestEntries.filter(({ config }) => config.pinDashboard).map(({ config }) => config.code), ...priorityEntries.filter(({ config }) => config.pinDashboard).map(({ config }) => config.code) ], sortByLatency: shouldSortByLatency(section) }); const latencyTestCodes = sortedOutbounds.filter( (outbound) => outbound.runtimeAvailable !== false && !isSelectorOutbound(outbound) && !outbound.priorityInfo ).map((outbound) => outbound.code); return { selector, latencyTestCode: selector?.code || builtInUrltestCode, latencyTestCodes: latencyTestCodes.length > 0 ? latencyTestCodes : void 0, outbounds: sortedOutbounds }; } function metadataMatchesCurrentSource(sectionName, sourceCount, metadata) { const legacyMetadata = metadata; const sourceIndex = metadata.sourceIndex ?? legacyMetadata.source_index; const sourceSection = metadata.sourceSection || legacyMetadata.source_section || ""; const hasSourceIndex = typeof sourceIndex === "number"; const hasSourceSection = sourceSection !== ""; if (!hasSourceIndex && !hasSourceSection) { return sourceCount <= 1; } if (sourceCount > 1 && !hasSourceSection) { return false; } if (hasSourceIndex && (sourceIndex < 1 || sourceIndex > sourceCount)) { return false; } if (hasSourceSection) { const expectedSourcePrefix = `${sectionName}-subscription-`; if (!sourceSection.startsWith(expectedSourcePrefix)) { return false; } const sourceSectionIndex = Number( sourceSection.slice(expectedSourcePrefix.length) ); if (!Number.isInteger(sourceSectionIndex) || sourceSectionIndex < 1 || sourceSectionIndex > sourceCount) { return false; } if (hasSourceIndex && sourceIndex !== sourceSectionIndex) { return false; } } return true; } function getSubscriptionMetadataSourceIndex(sectionName, sourceCount, metadata) { const legacyMetadata = metadata; const sourceIndex = metadata.sourceIndex ?? legacyMetadata.source_index; if (typeof sourceIndex === "number") { return sourceIndex; } const sourceSection = metadata.sourceSection || legacyMetadata.source_section || ""; const expectedSourcePrefix = `${sectionName}-subscription-`; if (sourceSection.startsWith(expectedSourcePrefix)) { const parsed = Number(sourceSection.slice(expectedSourcePrefix.length)); return Number.isInteger(parsed) ? parsed : void 0; } return sourceCount <= 1 ? 1 : void 0; } function isSubscriptionMetadataVisible(section, sourceCount, metadata) { const sourceIndex = getSubscriptionMetadataSourceIndex( section[".name"], sourceCount, metadata ); if (!sourceIndex || sourceIndex < 1 || sourceIndex > sourceCount) { return true; } const sourceEntry = getListValues(section.subscription_urls)[sourceIndex - 1]; const settings = itemSettingsMap(section.subscription_url_settings)[sourceEntry]; return settings?.show_dashboard_metadata !== "0"; } function getSubscriptionMetadata(section, sourceCount, dashboardCache) { if (!dashboardCache?.subscriptionMetadata) { return void 0; } const metadataItems = Array.isArray(dashboardCache.subscriptionMetadata) ? dashboardCache.subscriptionMetadata : [dashboardCache.subscriptionMetadata]; const visibleMetadataItems = metadataItems.filter( (metadata) => metadata && Object.keys(metadata).length > 1 && metadataMatchesCurrentSource(section[".name"], sourceCount, metadata) && isSubscriptionMetadataVisible(section, sourceCount, metadata) ); if (visibleMetadataItems.length > 0) { return visibleMetadataItems; } return void 0; } function getOutboundMetadata(dashboardCache) { const metadata = dashboardCache?.outboundMetadata; if (!metadata || typeof metadata !== "object") { return void 0; } return { names: objectMap(metadata.names), countries: objectMap(metadata.countries) }; } function getCachedProxyLinks(dashboardCache) { return new Map( Object.entries(objectMap(dashboardCache?.links)).filter( ([, link]) => isCopyableProxyLink(link) ) ); } async function getDashboardSections(options = {}) { const includeSubscriptionCopyState = options.includeSubscriptionCopyState ?? true; const configSections = hydrateConfigSections(await getConfigSections()); const clashProxies = await getClashApiProxies(configSections); if (!clashProxies.success || !clashProxies.data?.proxies) { return { success: false, data: [] }; } const proxies = Object.entries(clashProxies.data.proxies).map( ([key, value]) => ({ code: key, value }) ); const data = await Promise.all( configSections.filter( (section) => section.enabled !== "0" && (isConnectionAction(section.action) || isServiceAction(section.action)) ).map(async (section) => { const displayName = getDisplayName(section); const sectionName = section[".name"]; const sectionAction = section.action; const proxyConfigType = getSectionProxyConfigType(section); if (isConnectionAction(sectionAction) && shouldUseProxyGroup(section)) { const subscriptionSourceCount = getSubscriptionSourceCount(section); const subscriptionEnabled = subscriptionSourceCount > 0; const dashboardCache = await readDashboardSectionCache(sectionName); const outboundMetadata = getOutboundMetadata(dashboardCache); const subscriptionMetadata = subscriptionEnabled ? getSubscriptionMetadata( section, subscriptionSourceCount, dashboardCache ) : void 0; const cachedProxyLinks = includeSubscriptionCopyState ? getCachedProxyLinks(dashboardCache) : /* @__PURE__ */ new Map(); const urltestGroups = getUrlTestGroups(dashboardCache); const priorityGroups = getPriorityGroups(dashboardCache); const { selector, latencyTestCode, latencyTestCodes, outbounds } = buildProxyGroupOutbounds( section, proxies, outboundMetadata, urltestGroups, priorityGroups, cachedProxyLinks ); return { withTagSelect: true, code: selector?.code || sectionName, sectionName, displayName, action: sectionAction, latencyTestCode, latencyTestCodes, proxyConfigType, subscriptionSourceCount, subscriptionMetadata, outbounds }; } if (sectionAction === "vpn" || sectionAction === "awg" || sectionAction === "warp") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find((proxy) => proxy.code === outboundTag); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, action: sectionAction, outbounds: [ { code: outbound?.code || sectionName, displayName: section.interface || outbound?.value?.name || (sectionAction === "awg" ? "AmneziaWG" : sectionAction.toUpperCase()), latency: outbound?.value?.history?.length ? outbound.value.history[0].delay > 0 ? outbound.value.history[0].delay : -1 : 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false, runtimeAvailable: Boolean(outbound) } ] }; } if (sectionAction === "outbound") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find((proxy) => proxy.code === outboundTag); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, action: sectionAction, outbounds: [ { code: outbound?.code || sectionName, displayName: getJsonOutboundDisplayName(section) || outbound?.value?.name || "", latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false } ] }; } if (isServiceAction(sectionAction)) { const serviceType = sectionAction; let serviceStatus; try { if (serviceType === "zapret") { const result = await TachyonShellMethods.getZapretStatus(); if (result.success) { const s = result.data; serviceStatus = { serviceType: "zapret", configured: Boolean(s.configured), ready: Boolean(s.ready), conflict: Boolean(s.conflict), runningProcesses: s.running_process_count, expectedProcesses: s.expected_process_count, restartCount: s.restart_count, unstable: Boolean(s.runtime_unstable), statusMessage: s.status_message }; } } else if (serviceType === "zapret2") { const result = await TachyonShellMethods.getZapret2Status(); if (result.success) { const s = result.data; serviceStatus = { serviceType: "zapret2", configured: Boolean(s.configured), ready: Boolean(s.ready), conflict: Boolean(s.conflict), runningProcesses: s.running_process_count, expectedProcesses: s.expected_process_count, restartCount: 0, unstable: false, statusMessage: s.status_message }; } } else if (serviceType === "byedpi") { const result = await TachyonShellMethods.getByedpiStatus(); if (result.success) { const s = result.data; serviceStatus = { serviceType: "byedpi", configured: Boolean(s.configured), ready: Boolean(s.ready), conflict: Boolean(s.conflict), runningProcesses: s.running_process_count, expectedProcesses: s.expected_process_count, restartCount: s.restart_count, unstable: Boolean(s.runtime_unstable), statusMessage: s.status_message }; } } } catch (_error) { } const statusLabel = serviceStatus ? serviceStatus.ready ? _("Running") : serviceStatus.conflict ? _("Conflict") : serviceStatus.configured ? _("Stopped") : _("Not configured") : _("Unknown"); return { withTagSelect: false, code: sectionName, sectionName, displayName, action: sectionAction, serviceStatus, outbounds: [ { code: sectionName, displayName: statusLabel, latency: 0, type: serviceType.toUpperCase(), selected: true, canCopyLink: false } ] }; } return { withTagSelect: false, code: sectionName, sectionName, displayName, action: sectionAction, outbounds: [] }; }) ); return { success: true, data }; } // src/tachyon/methods/custom/getClashApiSecret.ts async function getClashApiSecret2() { const sections = await getConfigSections(); const settings = sections.find((section) => section[".type"] === "settings"); return settings?.yacd_secret_key || ""; } // src/tachyon/methods/custom/index.ts var CustomTachyonMethods = { getConfigSections, getDashboardSections, getClashApiSecret: getClashApiSecret2 }; // src/tachyon/api.ts async function createBaseApiRequest(fetchFn, options) { const wrappedFn = () => options?.timeoutMs && options?.operationName ? withTimeout( fetchFn(), options.timeoutMs, options.operationName, options.timeoutMessage ) : fetchFn(); try { const response = await wrappedFn(); if (!response.ok) { return { success: false, message: `${_("HTTP error")} ${response.status}: ${response.statusText}` }; } const data = await response.json(); return { success: true, data }; } catch (e) { return { success: false, message: e instanceof Error ? e.message : _("Unknown error") }; } } // src/tachyon/methods/fakeip/getFakeIpCheck.ts async function getFakeIpCheck() { return createBaseApiRequest( () => fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { method: "GET", headers: { "Content-Type": "application/json" } }), { operationName: "getFakeIpCheck", timeoutMs: 5e3 } ); } // src/tachyon/methods/fakeip/getIpCheck.ts async function getIpCheck() { return createBaseApiRequest( () => fetch(`https://${IP_CHECK_DOMAIN}/check`, { method: "GET", headers: { "Content-Type": "application/json" } }), { operationName: "getIpCheck", timeoutMs: 5e3 } ); } // src/tachyon/methods/fakeip/index.ts var RemoteFakeIPMethods = { getFakeIpCheck, getIpCheck }; // src/tachyon/services/tab.service.ts var TabService = class _TabService { constructor() { this.observer = null; this.lastActiveId = null; this.init(); } static getInstance() { if (!_TabService.instance) { _TabService.instance = new _TabService(); } return _TabService.instance; } init() { this.observer = new MutationObserver(() => this.handleMutations()); this.observer.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ["class"] }); this.notify(); } handleMutations() { this.notify(); } getTabsInfo() { const tabs = Array.from( document.querySelectorAll(".cbi-tab, .cbi-tab-disabled") ); return tabs.map((el) => ({ el, id: el.dataset.tab || "", active: el.classList.contains("cbi-tab") && !el.classList.contains("cbi-tab-disabled") })); } getActiveTabId() { const active = document.querySelector( ".cbi-tab:not(.cbi-tab-disabled)" ); return active?.dataset.tab || null; } notify() { const tabs = this.getTabsInfo(); const activeId = this.getActiveTabId(); if (activeId !== this.lastActiveId) { this.lastActiveId = activeId; this.callback?.(activeId, tabs); } } onChange(callback) { this.callback = callback; this.notify(); } }; var TabServiceInstance = TabService.getInstance(); // src/tachyon/tabs/diagnostic/helpers/getCheckTitle.ts function getCheckTitle(name) { return `${name} ${_("checks")}`; } // src/tachyon/tabs/diagnostic/checks/contstants.ts var DIAGNOSTICS_CHECKS = /* @__PURE__ */ ((DIAGNOSTICS_CHECKS2) => { DIAGNOSTICS_CHECKS2["DNS"] = "DNS"; DIAGNOSTICS_CHECKS2["SINGBOX"] = "SINGBOX"; DIAGNOSTICS_CHECKS2["NFT"] = "NFT"; DIAGNOSTICS_CHECKS2["ZAPRET"] = "ZAPRET"; DIAGNOSTICS_CHECKS2["ZAPRET2"] = "ZAPRET2"; DIAGNOSTICS_CHECKS2["BYEDPI"] = "BYEDPI"; DIAGNOSTICS_CHECKS2["FAKEIP"] = "FAKEIP"; DIAGNOSTICS_CHECKS2["OUTBOUNDS"] = "OUTBOUNDS"; DIAGNOSTICS_CHECKS2["INBOUNDS"] = "INBOUNDS"; return DIAGNOSTICS_CHECKS2; })(DIAGNOSTICS_CHECKS || {}); var DIAGNOSTICS_CHECKS_MAP = { ["DNS" /* DNS */]: { order: 1, title: getCheckTitle("DNS"), code: "DNS" /* DNS */ }, ["SINGBOX" /* SINGBOX */]: { order: 2, title: getCheckTitle("Sing-box"), code: "SINGBOX" /* SINGBOX */ }, ["NFT" /* NFT */]: { order: 4, title: getCheckTitle("Nftables"), code: "NFT" /* NFT */ }, ["ZAPRET" /* ZAPRET */]: { order: 5, title: getCheckTitle("Zapret"), code: "ZAPRET" /* ZAPRET */ }, ["BYEDPI" /* BYEDPI */]: { order: 7, title: getCheckTitle("ByeDPI"), code: "BYEDPI" /* BYEDPI */ }, ["ZAPRET2" /* ZAPRET2 */]: { order: 6, title: getCheckTitle("Zapret2"), code: "ZAPRET2" /* ZAPRET2 */ }, ["OUTBOUNDS" /* OUTBOUNDS */]: { order: 8, title: getCheckTitle("Outbounds"), code: "OUTBOUNDS" /* OUTBOUNDS */ }, ["FAKEIP" /* FAKEIP */]: { order: 9, title: getCheckTitle("FakeIP"), code: "FAKEIP" /* FAKEIP */ }, ["INBOUNDS" /* INBOUNDS */]: { order: 3, title: getCheckTitle("Inbounds"), code: "INBOUNDS" /* INBOUNDS */ } }; // src/tachyon/tabs/diagnostic/diagnostic.store.ts function createDiagnosticCheck(code, description) { const meta = DIAGNOSTICS_CHECKS_MAP[code]; return { code, title: meta.title, order: meta.order, description, items: [], state: "skipped" }; } function getDiagnosticsChecks(description, options = {}) { const checks = ["DNS" /* DNS */, "SINGBOX" /* SINGBOX */]; if (options.includeInbounds === true) { checks.push("INBOUNDS" /* INBOUNDS */); } checks.push("NFT" /* NFT */); if (options.includeZapret) { checks.push("ZAPRET" /* ZAPRET */); } if (options.includeZapret2) { checks.push("ZAPRET2" /* ZAPRET2 */); } if (options.includeByedpi) { checks.push("BYEDPI" /* BYEDPI */); } checks.push("OUTBOUNDS" /* OUTBOUNDS */, "FAKEIP" /* FAKEIP */); return checks.map((code) => createDiagnosticCheck(code, description)); } function getLoadingDiagnosticsChecks(options = {}) { return { diagnosticsChecks: getDiagnosticsChecks(_("Pending"), options) }; } var initialDiagnosticStore = { diagnosticsSystemInfo: { loading: true, loaded: false, providerInfoLoaded: false, tachyon_version: "loading", tachyon_commit_sha: "", tachyon_latest_version: "loading", luci_app_version: "loading", sing_box_version: "loading", sing_box_extended: 0, sing_box_tiny: 0, sing_box_compressed: 0, sing_box_lx: 0, sing_box_tailscale: 1, sing_box_repo_url: "", zapret_version: "loading", zapret_installed: 0, zapret2_version: "loading", zapret2_installed: 0, byedpi_version: "loading", byedpi_installed: 0, server_inbounds_enabled_count: -1, openwrt_version: "loading", device_model: "loading" }, diagnosticsActions: { restart: { loading: false }, start: { loading: false }, stop: { loading: false }, enable: { loading: false }, disable: { loading: false }, globalCheck: { loading: false }, doctor: { loading: false }, aiDoctor: { loading: false }, viewLogs: { loading: false }, showSingBoxConfig: { loading: false }, generateBugReport: { loading: false }, checkServices: { loading: false } }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: getDiagnosticsChecks(_("Not running")), updatesActions: { tachyonCheck: { loading: false }, tachyonInstall: { loading: false }, tachyonReinstall: { loading: false }, singBoxCheck: { loading: false }, singBoxInstall: { loading: false }, singBoxInstallExtended: { loading: false }, singBoxInstallExtendedCompressed: { loading: false }, singBoxInstallLx: { loading: false }, singBoxInstallTiny: { loading: false }, singBoxInstallStable: { loading: false }, zapretCheck: { loading: false }, zapretInstall: { loading: false }, zapretRemove: { loading: false }, zapret2Check: { loading: false }, zapret2Install: { loading: false }, zapret2Remove: { loading: false }, byedpiCheck: { loading: false }, byedpiInstall: { loading: false }, byedpiRemove: { loading: false } }, updatesChecks: { tachyon: { status: null, latest_version: "", release_url: "" }, sing_box: { status: null, latest_version: "", release_url: "" }, zapret: { status: null, latest_version: "", release_url: "" }, zapret2: { status: null, latest_version: "", release_url: "" }, byedpi: { status: null, latest_version: "", release_url: "" } } }; // src/tachyon/services/store.service.ts function jsonStableStringify(obj) { return JSON.stringify(obj, (_2, value) => { if (value && typeof value === "object" && !Array.isArray(value)) { return Object.keys(value).sort().reduce( (acc, key) => { acc[key] = value[key]; return acc; }, {} ); } return value; }); } function jsonEqual(a, b) { try { return jsonStableStringify(a) === jsonStableStringify(b); } catch { return false; } } var StoreService = class { constructor(initial) { this.listeners = /* @__PURE__ */ new Set(); this.value = initial; this.initial = structuredClone(initial); } get() { return this.value; } set(next) { const prev = this.value; const diff = {}; for (const key in next) { if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; } if (Object.keys(diff).length === 0) { return; } const merged = { ...prev, ...next }; this.value = merged; this.listeners.forEach((cb) => cb(this.value, prev, diff)); } reset(keys) { const prev = this.value; const next = structuredClone(this.value); if (keys && keys.length > 0) { keys.forEach((key) => { next[key] = structuredClone(this.initial[key]); }); } else { Object.assign(next, structuredClone(this.initial)); } if (jsonEqual(prev, next)) return; this.value = next; const diff = {}; for (const key in next) { if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; } this.listeners.forEach((cb) => cb(this.value, prev, diff)); } subscribe(cb) { this.listeners.add(cb); cb(this.value, this.value, {}); return () => this.listeners.delete(cb); } unsubscribe(cb) { this.listeners.delete(cb); } patch(key, value) { this.set({ [key]: value }); } getKey(key) { return this.value[key]; } subscribeKey(key, cb) { let prev = this.value[key]; const wrapper = (val) => { if (!jsonEqual(val[key], prev)) { prev = val[key]; cb(val[key]); } }; this.listeners.add(wrapper); return () => this.listeners.delete(wrapper); } }; var initialStore = { tabService: { current: "", all: [] }, bandwidthWidget: { loading: true, failed: false, data: { up: 0, down: 0 } }, trafficTotalWidget: { loading: true, failed: false, data: { downloadTotal: 0, uploadTotal: 0 } }, systemInfoWidget: { loading: true, failed: false, data: { connections: 0, memory: 0 } }, servicesInfoWidget: { loading: true, failed: false, data: { singbox: 0, tachyonRunning: 0, tachyonEnabled: 0, tachyonStatus: "", watchdogRunning: 0 } }, sectionsWidget: { loading: true, failed: false, latencyFetchingSections: {}, latencyProgressSections: {}, selectorSwitchingSections: {}, subscriptionUpdatingSections: {}, data: [] }, ...initialDiagnosticStore }; var store = new StoreService(initialStore); // src/tachyon/services/tachyonLogWatcher.service.ts var TachyonLogWatcher = class _TachyonLogWatcher { constructor() { this.intervalMs = 5e3; this.lastLines = /* @__PURE__ */ new Set(); this.maxTrackedLines = 500; this.running = false; this.paused = false; this.checking = false; if (typeof document !== "undefined") { document.addEventListener("visibilitychange", () => { if (document.hidden) this.pause(); else this.resume(); }); } } static getInstance() { if (!_TachyonLogWatcher.instance) { _TachyonLogWatcher.instance = new _TachyonLogWatcher(); } return _TachyonLogWatcher.instance; } init(fetcher, options) { this.fetcher = fetcher; this.onNewLog = options?.onNewLog; this.intervalMs = options?.intervalMs ?? 5e3; this.maxTrackedLines = options?.maxTrackedLines ?? 500; this.lastLines = /* @__PURE__ */ new Set(); logger.info( "[TachyonLogWatcher]", `initialized (interval: ${this.intervalMs}ms)` ); } normalizeLines(raw) { return raw.split("\n").filter(Boolean).slice(-this.maxTrackedLines); } async checkOnce() { if (!this.fetcher) { logger.warn("[TachyonLogWatcher]", "fetcher not found"); return; } if (this.paused) { logger.debug("[TachyonLogWatcher]", "skipped check \u2014 tab not visible"); return; } if (this.checking) { logger.debug( "[TachyonLogWatcher]", "skipped check \u2014 previous check is running" ); return; } this.checking = true; try { const raw = await this.fetcher(); const lines = this.normalizeLines(raw); for (const line of lines) { if (this.lastLines.has(line)) { continue; } this.lastLines.add(line); this.onNewLog?.(line); } if (this.lastLines.size > this.maxTrackedLines) { this.lastLines = new Set( Array.from(this.lastLines).slice(-this.maxTrackedLines) ); } } catch (err) { logger.error("[TachyonLogWatcher]", "failed to read logs:", err); } finally { this.checking = false; } } start() { if (this.running) return; if (!this.fetcher) { logger.warn("[TachyonLogWatcher]", "attempted to start without fetcher"); return; } this.running = true; void this.checkOnce(); this.timer = setInterval(() => this.checkOnce(), this.intervalMs); logger.info( "[TachyonLogWatcher]", `started (interval: ${this.intervalMs}ms)` ); } stop() { if (!this.running) return; this.running = false; if (this.timer) clearInterval(this.timer); logger.info("[TachyonLogWatcher]", "stopped"); } pause() { if (!this.running || this.paused) return; this.paused = true; logger.info("[TachyonLogWatcher]", "paused (tab not visible)"); } resume() { if (!this.running || !this.paused) return; this.paused = false; logger.info("[TachyonLogWatcher]", "resumed (tab active)"); void this.checkOnce(); } reset() { this.lastLines = /* @__PURE__ */ new Set(); this.checking = false; logger.info("[TachyonLogWatcher]", "log history reset"); } }; // src/tachyon/services/logNotificationDeduper.service.ts var LOG_NOTIFICATION_STORAGE_KEY = "tachyon:shown-log-error-notifications:v1"; var MAX_STORED_LOG_NOTIFICATIONS = 500; function getSessionStorage() { if (typeof window === "undefined") { return null; } try { return window.sessionStorage; } catch { return null; } } function readStoredKeys(storage) { if (!storage) { return []; } try { const parsed = JSON.parse( storage.getItem(LOG_NOTIFICATION_STORAGE_KEY) || "[]" ); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []; } catch { return []; } } function writeStoredKeys(storage, keys) { if (!storage) { return; } try { storage.setItem( LOG_NOTIFICATION_STORAGE_KEY, JSON.stringify(keys.slice(-MAX_STORED_LOG_NOTIFICATIONS)) ); } catch { } } var SUPPRESSED_LOG_PATTERNS = [ /another component action is already running/i, /subscription dns resolution failed for rule.*retrying in/i ]; function isErrorLogLine(line) { const lower = line.toLowerCase(); if (SUPPRESSED_LOG_PATTERNS.some((pattern) => pattern.test(lower))) { return false; } return lower.includes("[error]") || lower.includes("[fatal]") || lower.includes("sing-box") && lower.includes("rule-set") && /\b(error|fatal)\b/.test(lower); } function getTachyonLogNotification(line) { if (isErrorLogLine(line)) { return { kind: "error", line }; } const update = line.match( /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi)\s+(\S+)/i ); if (!update) { return null; } return { kind: "component-update", line, component: update[1].toLowerCase(), version: update[2] }; } function getLogNotificationKey(line) { return line.trim(); } var LogNotificationDeduper = class { constructor(storage = getSessionStorage()) { this.storage = storage; this.seenKeys = new Set(readStoredKeys(storage)); } shouldNotify(line) { if (!getTachyonLogNotification(line)) { return false; } const key = getLogNotificationKey(line); if (!key || this.seenKeys.has(key)) { return false; } this.seenKeys.add(key); writeStoredKeys(this.storage, Array.from(this.seenKeys)); return true; } }; // src/tachyon/helpers/getComponentActionKey.ts var componentActionKeyMap = { "tachyon:check_update": "tachyonCheck", "tachyon:install": "tachyonInstall", "tachyon:reinstall": "tachyonReinstall", "sing_box:check_update": "singBoxCheck", "sing_box:install": "singBoxInstall", "sing_box:install_extended": "singBoxInstallExtended", "sing_box:install_extended_compressed": "singBoxInstallExtendedCompressed", "sing_box:install_lx": "singBoxInstallLx", "sing_box:install_tiny": "singBoxInstallTiny", "sing_box:install_stable": "singBoxInstallStable", "zapret:check_update": "zapretCheck", "zapret:install": "zapretInstall", "zapret:remove": "zapretRemove", "zapret2:check_update": "zapret2Check", "zapret2:install": "zapret2Install", "zapret2:remove": "zapret2Remove", "byedpi:check_update": "byedpiCheck", "byedpi:install": "byedpiInstall", "byedpi:remove": "byedpiRemove" }; function getComponentActionKey(component, action) { return componentActionKeyMap[`${component}:${action}`]; } // src/tachyon/helpers/singBoxVariant.ts function isExtendedSingBoxVersion(version) { return String(version || "").includes("extended") || String(version || "").includes("-lx"); } function isVersionPlaceholder(version) { const normalized = String(version || "").trim().toLowerCase(); if (!normalized || normalized === "loading" || normalized === "unknown" || normalized === "not installed") { return true; } return typeof _ === "function" && normalized === _("unknown").toLowerCase() || typeof _ === "function" && normalized === _("Not installed").toLowerCase(); } function formatSingBoxVersion(value) { const version = String(value.sing_box_version || ""); if (!version || version === "not installed") { return _("Not installed"); } if (isVersionPlaceholder(version)) { return version; } const normalizedValue = normalizeSingBoxVariantFields(value); let variant = ""; if (normalizedValue.sing_box_extended && normalizedValue.sing_box_compressed) { variant = _("compressed"); } else if (normalizedValue.sing_box_extended && normalizedValue.sing_box_lx) { variant = _("lx"); } else if (normalizedValue.sing_box_tiny) { variant = _("tiny"); } return variant ? `${version} (${variant})` : version; } function normalizeSingBoxVariantFields(value) { const versionExtended = isExtendedSingBoxVersion(value.sing_box_version); const singBoxExtended = Boolean(value.sing_box_extended) || versionExtended; return { ...value, sing_box_extended: singBoxExtended ? 1 : 0, sing_box_tiny: singBoxExtended ? 0 : value.sing_box_tiny ? 1 : 0, sing_box_compressed: singBoxExtended && value.sing_box_compressed ? 1 : 0, sing_box_lx: singBoxExtended && value.sing_box_lx ? 1 : 0, sing_box_tailscale: singBoxExtended || value.sing_box_tailscale ? 1 : 0 }; } // src/tachyon/services/localActionOverlay.service.ts var componentActions = /* @__PURE__ */ new Set(); var subscriptionSections = /* @__PURE__ */ new Set(); var latencySections = /* @__PURE__ */ new Set(); var serviceActions = /* @__PURE__ */ new Set(); function setMembership(set, value, active) { if (active) { set.add(value); } else { set.delete(value); } } function setLocalComponentAction(action, active) { setMembership(componentActions, action, active); } function setLocalSubscriptionAction(section, active) { if (section) { setMembership(subscriptionSections, section, active); } } function setLocalLatencyAction(section, active) { if (section) { setMembership(latencySections, section, active); } } function setLocalServiceAction(action, active) { if (action === "restart" || action === "start" || action === "stop") { setMembership(serviceActions, action, active); } } function getLocalActionOverlay() { return { componentActions: new Set(componentActions), subscriptionSections: new Set(subscriptionSections), latencySections: new Set(latencySections), serviceActions: new Set(serviceActions) }; } // src/tachyon/services/uiState.service.ts function isRunningAction(state) { return state.running === true; } function getEmptyUpdatesActions() { return { tachyonCheck: { loading: false }, tachyonInstall: { loading: false }, tachyonReinstall: { loading: false }, singBoxCheck: { loading: false }, singBoxInstall: { loading: false }, singBoxInstallExtended: { loading: false }, singBoxInstallExtendedCompressed: { loading: false }, singBoxInstallLx: { loading: false }, singBoxInstallTiny: { loading: false }, singBoxInstallStable: { loading: false }, zapretCheck: { loading: false }, zapretInstall: { loading: false }, zapretRemove: { loading: false }, zapret2Check: { loading: false }, zapret2Install: { loading: false }, zapret2Remove: { loading: false }, byedpiCheck: { loading: false }, byedpiInstall: { loading: false }, byedpiRemove: { loading: false } }; } function getEmptyDiagnosticsActions() { return { ...store.get().diagnosticsActions, restart: { loading: false }, start: { loading: false }, stop: { loading: false } }; } function normalizeLatencyProgress(progress) { const total = Math.trunc(Number(progress?.total ?? 0)); if (!Number.isFinite(total) || total <= 0) { return void 0; } const completedValue = Number(progress?.completed ?? 0); const failedValue = Number(progress?.failed ?? 0); const completed = Number.isFinite(completedValue) ? Math.trunc(completedValue) : 0; const failed2 = Number.isFinite(failedValue) ? Math.trunc(failedValue) : 0; return { completed: Math.min(Math.max(0, completed), total), total, failed: Math.max(0, failed2) }; } function applyServiceState(uiState) { const currentSystemInfo = store.get().diagnosticsSystemInfo; const nextSystemInfo = { ...currentSystemInfo, providerInfoLoaded: true, zapret_installed: uiState.capabilities.zapret_installed, zapret2_installed: uiState.capabilities.zapret2_installed, byedpi_installed: uiState.capabilities.byedpi_installed, server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count }; nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended; nextSystemInfo.sing_box_tiny = uiState.capabilities.sing_box_tiny; nextSystemInfo.sing_box_compressed = uiState.capabilities.sing_box_compressed; nextSystemInfo.sing_box_lx = uiState.capabilities.sing_box_lx; nextSystemInfo.sing_box_tailscale = uiState.capabilities.sing_box_tailscale; store.set({ servicesInfoWidget: { loading: false, failed: false, data: { singbox: uiState.service.sing_box.running, tachyonRunning: uiState.service.tachyon.running, tachyonEnabled: uiState.service.tachyon.enabled, tachyonStatus: uiState.service.tachyon.status, watchdogRunning: store.get().servicesInfoWidget.data.watchdogRunning } }, diagnosticsSystemInfo: normalizeSingBoxVariantFields(nextSystemInfo) }); } function applyActionState(actions = {}) { const current = store.get(); const localOverlay = getLocalActionOverlay(); const currentLatencyProgressSections = current.sectionsWidget.latencyProgressSections; const subscriptionUpdatingSections = {}; const latencyFetchingSections = {}; const latencyProgressSections = {}; const updatesActions = getEmptyUpdatesActions(); const diagnosticsActions = getEmptyDiagnosticsActions(); for (const state of actions.subscription || []) { if (isRunningAction(state) && state.section) { subscriptionUpdatingSections[state.section] = true; } } for (const state of actions.latency || []) { if (isRunningAction(state) && state.section) { latencyFetchingSections[state.section] = true; const progress = normalizeLatencyProgress(state.progress); if (progress) { latencyProgressSections[state.section] = progress; } else if (currentLatencyProgressSections[state.section]) { latencyProgressSections[state.section] = currentLatencyProgressSections[state.section]; } } } for (const state of actions.component || []) { if (!isRunningAction(state)) { continue; } const key = getComponentActionKey(state.component, state.action); if (key) { updatesActions[key] = { loading: true }; } } for (const state of actions.service || []) { if (!isRunningAction(state)) { continue; } if (state.action === "start") { diagnosticsActions.start = { loading: true }; } else if (state.action === "stop") { diagnosticsActions.stop = { loading: true }; } else if (state.action === "restart" || state.action === "reload") { diagnosticsActions.restart = { loading: true }; } } for (const section of localOverlay.subscriptionSections) { subscriptionUpdatingSections[section] = true; } for (const section of localOverlay.latencySections) { latencyFetchingSections[section] = true; if (!latencyProgressSections[section] && currentLatencyProgressSections[section]) { latencyProgressSections[section] = currentLatencyProgressSections[section]; } } for (const key of localOverlay.componentActions) { updatesActions[key] = { loading: true }; } for (const action of localOverlay.serviceActions) { diagnosticsActions[action] = { loading: true }; } store.set({ sectionsWidget: { ...current.sectionsWidget, subscriptionUpdatingSections, latencyFetchingSections, latencyProgressSections }, updatesActions, diagnosticsActions }); } function applyUiStateToStore(uiState) { applyServiceState(uiState); applyActionState(uiState.actions); } // src/tachyon/services/runtimeUiState.service.ts var RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS = 500; var RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS = 1e3; var RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS = 500; var runtimeUiStateRefreshPromise = null; var lastRuntimeUiStateRefreshAt = 0; var lastRuntimeUiState; var runtimeStateResumeRefreshRegistered = false; var runtimeStatePollTimer = null; var runtimeStatePollingStarted = false; var runtimeStateHasRunningAction = false; var runtimeUiStateListeners = /* @__PURE__ */ new Set(); function isDocumentVisible() { return typeof document === "undefined" || !document.visibilityState || document.visibilityState === "visible"; } function hasRunningAction(uiState) { return Object.values(uiState.actions).some( (actions) => actions.some((action) => action.running) ); } function getNextPollDelay() { return runtimeStateHasRunningAction ? RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS : RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS; } function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) { if (!runtimeStatePollingStarted || runtimeStatePollTimer || typeof window === "undefined") { return; } runtimeStatePollTimer = window.setTimeout(() => { runtimeStatePollTimer = null; void refreshRuntimeUiState().catch(() => void 0).finally(() => { scheduleRuntimeUiStatePoll(); }); }, delay); } function notifyRuntimeUiStateListeners(uiState) { for (const listener of runtimeUiStateListeners) { try { listener(uiState); } catch (error) { logger.error("[RUNTIME_UI_STATE]", "listener failed", error); } } } async function refreshRuntimeUiState({ force = false } = {}) { if (!isDocumentVisible()) { return void 0; } if (runtimeUiStateRefreshPromise) { return runtimeUiStateRefreshPromise; } const now = Date.now(); if (!force && now - lastRuntimeUiStateRefreshAt < RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS) { return void 0; } lastRuntimeUiStateRefreshAt = now; const promise = TachyonShellMethods.getUiState().then((response) => { if (!response.success) { return void 0; } applyUiStateToStore(response.data); lastRuntimeUiState = response.data; runtimeStateHasRunningAction = hasRunningAction(response.data); notifyRuntimeUiStateListeners(response.data); return response.data; }).catch((error) => { logger.error("[RUNTIME_UI_STATE]", "refresh failed", error); return void 0; }).finally(() => { if (runtimeUiStateRefreshPromise === promise) { runtimeUiStateRefreshPromise = null; } }); runtimeUiStateRefreshPromise = promise; return runtimeUiStateRefreshPromise; } function subscribeRuntimeUiState(listener) { runtimeUiStateListeners.add(listener); if (lastRuntimeUiState) { try { listener(lastRuntimeUiState); } catch (error) { logger.error("[RUNTIME_UI_STATE]", "listener failed", error); } } return () => { runtimeUiStateListeners.delete(listener); }; } function getCachedRuntimeUiState() { return lastRuntimeUiState; } function registerRuntimeStateResumeRefresh() { if (runtimeStateResumeRefreshRegistered || typeof window === "undefined") { return; } runtimeStateResumeRefreshRegistered = true; const refreshOnResume = () => { if (!isDocumentVisible()) { return; } void refreshRuntimeUiState({ force: true }); }; document.addEventListener("visibilitychange", refreshOnResume); window.addEventListener("pageshow", refreshOnResume); window.addEventListener("focus", refreshOnResume); } function startRuntimeUiStatePolling() { if (runtimeStatePollingStarted || typeof window === "undefined") { return; } runtimeStatePollingStarted = true; void refreshRuntimeUiState({ force: true }).finally(() => { scheduleRuntimeUiStatePoll(); }); } // src/tachyon/services/core.service.ts var LOG_WATCHER_INTERVAL_MS = 1e4; var LOG_WATCHER_START_DELAY_MS = 5e3; function componentDisplayName(component) { const names = { tachyon: "Tachyon", sing_box: "sing-box", zapret: "Zapret", zapret2: "Zapret2", byedpi: "ByeDPI" }; return names[component] || component; } function showLogNotification(notification) { if (notification.kind === "component-update") { const message = _("New version %s is available for %s").replace("%s", notification.version).replace("%s", componentDisplayName(notification.component)); ui.addNotification( _("Component update available"), E("div", {}, message), "warning", "tachyon-component-update-notification" ); return; } ui.addNotification( _("Tachyon Error"), E("div", {}, notification.line), "error", "tachyon-log-error-notification" ); } function coreService(options = {}) { TabServiceInstance.onChange((activeId, tabs) => { logger.info("[TAB]", activeId); store.set({ tabService: { current: activeId || "", all: tabs.map((tab) => tab.id) } }); }); const watcher = TachyonLogWatcher.getInstance(); const logNotificationDeduper = new LogNotificationDeduper(); watcher.init( async () => { const logs = await TachyonShellMethods.checkLogs(); if (logs.success) { return logs.data; } return ""; }, { intervalMs: LOG_WATCHER_INTERVAL_MS, onNewLog: (line) => { if (logNotificationDeduper.shouldNotify(line)) { const notification = getTachyonLogNotification(line); if (notification) { showLogNotification(notification); } } } } ); const startWatcher = async () => { if (options.waitForLogWatcherStart) { await Promise.resolve().then(() => options.waitForLogWatcherStart?.()).catch(() => null); } watcher.start(); }; const scheduleStartWatcher = () => window.setTimeout(() => { void startWatcher(); }, options.logWatcherStartDelayMs ?? LOG_WATCHER_START_DELAY_MS); if (typeof window !== "undefined") { scheduleStartWatcher(); } else { void startWatcher(); } registerRuntimeStateResumeRefresh(); startRuntimeUiStatePolling(); } // src/tachyon/services/socket.service.ts var SocketManager = class _SocketManager { constructor() { this.sockets = /* @__PURE__ */ new Map(); this.listeners = /* @__PURE__ */ new Map(); this.connected = /* @__PURE__ */ new Map(); this.errorListeners = /* @__PURE__ */ new Map(); this.reconnectTimers = /* @__PURE__ */ new Map(); this.reconnectAttempts = /* @__PURE__ */ new Map(); } static getInstance() { if (!_SocketManager.instance) { _SocketManager.instance = new _SocketManager(); } return _SocketManager.instance; } resetAll() { for (const [url, ws] of this.sockets.entries()) { try { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); } } catch (err) { logger.error( "[SOCKET]", `resetAll: failed to close socket ${url}`, err ); } } for (const timer of this.reconnectTimers.values()) { clearTimeout(timer); } this.reconnectTimers.clear(); this.reconnectAttempts.clear(); this.sockets.clear(); this.listeners.clear(); this.errorListeners.clear(); this.connected.clear(); logger.info("[SOCKET]", "All connections and state have been reset."); } scheduleReconnect(url) { if (this.reconnectTimers.has(url) || (this.listeners.get(url)?.size || 0) === 0) { return; } const attempts = this.reconnectAttempts.get(url) || 0; if (attempts >= 10) { logger.error( "[SOCKET]", `Max reconnect attempts (10) reached for ${url}` ); return; } const delay = Math.min(1e3 * Math.pow(2, attempts), 3e4); this.reconnectAttempts.set(url, attempts + 1); logger.info( "[SOCKET]", `Scheduling reconnect to ${url} in ${delay}ms (attempt ${attempts + 1})` ); const timer = setTimeout(() => { this.reconnectTimers.delete(url); if ((this.listeners.get(url)?.size || 0) > 0) { logger.info("[SOCKET]", `Attempting reconnect to ${url}`); this.connect(url); } }, delay); this.reconnectTimers.set(url, timer); } connect(url) { if (this.sockets.has(url)) return; let ws; try { ws = new WebSocket(url); } catch (err) { logger.error( "[SOCKET]", `failed to construct WebSocket for ${url}:`, err ); this.triggerError(url, err instanceof Event ? err : String(err)); this.scheduleReconnect(url); return; } this.sockets.set(url, ws); this.connected.set(url, false); if (!this.listeners.has(url)) this.listeners.set(url, /* @__PURE__ */ new Set()); if (!this.errorListeners.has(url)) this.errorListeners.set(url, /* @__PURE__ */ new Set()); ws.addEventListener("open", () => { this.connected.set(url, true); this.reconnectAttempts.set(url, 0); logger.info("[SOCKET]", "Connected to", url); }); ws.addEventListener("message", (event) => { const handlers = this.listeners.get(url); if (handlers) { for (const handler of handlers) { try { handler(event.data); } catch (err) { logger.error("[SOCKET]", `Handler error for ${url}:`, err); } } } }); ws.addEventListener("close", () => { this.connected.set(url, false); logger.warn("[SOCKET]", `Disconnected: ${url}`); this.triggerError(url, "Connection closed"); if (this.sockets.get(url) === ws) { this.sockets.delete(url); this.scheduleReconnect(url); } }); ws.addEventListener("error", (err) => { logger.error("[SOCKET]", `Socket error for ${url}:`, err); this.triggerError(url, err); }); } subscribe(url, listener, onError) { if (!this.errorListeners.has(url)) { this.errorListeners.set(url, /* @__PURE__ */ new Set()); } if (onError) { this.errorListeners.get(url)?.add(onError); } if (!this.listeners.has(url)) { this.listeners.set(url, /* @__PURE__ */ new Set()); } this.listeners.get(url)?.add(listener); if (!this.sockets.has(url)) { this.connect(url); } } unsubscribe(url, listener, onError) { this.listeners.get(url)?.delete(listener); if (onError) { this.errorListeners.get(url)?.delete(onError); } if (this.listeners.get(url)?.size === 0) { this.disconnect(url); } } // eslint-disable-next-line send(url, data) { const ws = this.sockets.get(url); if (ws && this.connected.get(url)) { ws.send(typeof data === "string" ? data : JSON.stringify(data)); } else { logger.warn("[SOCKET]", `Cannot send: not connected to ${url}`); this.triggerError(url, "Not connected"); } } disconnect(url) { const timer = this.reconnectTimers.get(url); if (timer) { clearTimeout(timer); this.reconnectTimers.delete(url); } this.reconnectAttempts.delete(url); const ws = this.sockets.get(url); if (ws) { ws.close(); this.sockets.delete(url); this.listeners.delete(url); this.errorListeners.delete(url); this.connected.delete(url); } } disconnectAll() { for (const url of this.sockets.keys()) { this.disconnect(url); } } triggerError(url, err) { const handlers = this.errorListeners.get(url); if (handlers) { for (const cb of handlers) { try { cb(err); } catch (e) { logger.error("[SOCKET]", `Error handler threw for ${url}:`, e); } } } } }; var socket = SocketManager.getInstance(); // src/tachyon/services/uiActionNotification.service.ts var UI_ACTION_NOTIFICATION_STORAGE_KEY = "tachyon:owned-ui-action-notifications:v1"; var MAX_STORED_UI_ACTION_NOTIFICATIONS = 100; function getSessionStorage2() { if (typeof window === "undefined") { return null; } try { return window.sessionStorage; } catch { return null; } } function getNotificationKey(kind, jobId) { return `${kind}:${jobId}`; } function isStoredNotification(value) { if (!value || typeof value !== "object") { return false; } const candidate = value; return (candidate.kind === "component" || candidate.kind === "subscription") && typeof candidate.jobId === "string" && typeof candidate.notified === "boolean" && typeof candidate.updatedAt === "number"; } function readStoredNotifications(storage) { if (!storage) { return []; } try { const parsed = JSON.parse( storage.getItem(UI_ACTION_NOTIFICATION_STORAGE_KEY) || "[]" ); return Array.isArray(parsed) ? parsed.filter(isStoredNotification) : []; } catch { return []; } } function writeStoredNotifications(storage, notifications) { if (!storage) { return; } try { storage.setItem( UI_ACTION_NOTIFICATION_STORAGE_KEY, JSON.stringify( notifications.sort((a, b) => a.updatedAt - b.updatedAt).slice(-MAX_STORED_UI_ACTION_NOTIFICATIONS) ) ); } catch { } } var UiActionNotificationTracker = class { constructor(storage = getSessionStorage2()) { this.notifications = /* @__PURE__ */ new Map(); this.storage = storage; for (const notification of readStoredNotifications(storage)) { this.notifications.set( getNotificationKey(notification.kind, notification.jobId), notification ); } } markOwned(kind, jobId) { if (!jobId) { return; } const key = getNotificationKey(kind, jobId); const current = this.notifications.get(key); this.notifications.set(key, { kind, jobId, notified: current?.notified ?? false, updatedAt: Date.now() }); this.persist(); } shouldNotify(kind, jobId) { if (!jobId) { return false; } const key = getNotificationKey(kind, jobId); const current = this.notifications.get(key); if (!current || current.notified) { return false; } this.notifications.set(key, { ...current, notified: true, updatedAt: Date.now() }); this.persist(); return true; } persist() { writeStoredNotifications( this.storage, Array.from(this.notifications.values()) ); } }; var uiActionNotifications = new UiActionNotificationTracker(); function markUiActionOwned(kind, jobId) { uiActionNotifications.markOwned(kind, jobId); } function shouldNotifyOwnedUiAction(kind, jobId) { return uiActionNotifications.shouldNotify(kind, jobId); } // src/tachyon/fetchers/fetchServicesInfo.ts var latestServicesInfoRequestId = 0; function getSettledMethodResponse(scope, result) { if (result.status === "fulfilled") { return result.value; } logger.error("[SERVICES_INFO]", `${scope} failed`, result.reason); return { success: false, error: result.reason instanceof Error ? result.reason.message : "" }; } async function fetchServicesInfo() { const requestId = ++latestServicesInfoRequestId; const uiState = await refreshRuntimeUiState({ force: true }); if (requestId !== latestServicesInfoRequestId) { return; } if (uiState) { return uiState; } const [tachyonResult, singboxResult, watchdogResult] = await Promise.allSettled([ TachyonShellMethods.getStatus(), TachyonShellMethods.getSingBoxStatus(), TachyonShellMethods.getWatchdogStatus() ]); if (requestId !== latestServicesInfoRequestId) { return; } const tachyon = getSettledMethodResponse("getStatus", tachyonResult); const singbox = getSettledMethodResponse("getSingBoxStatus", singboxResult); const watchdog = getSettledMethodResponse( "getWatchdogStatus", watchdogResult ); const previousData = store.get().servicesInfoWidget.data; store.set({ servicesInfoWidget: { loading: false, failed: !tachyon.success || !singbox.success, data: { singbox: singbox.success ? singbox.data.running : previousData.singbox, tachyonRunning: tachyon.success ? tachyon.data.running : previousData.tachyonRunning, tachyonEnabled: tachyon.success ? tachyon.data.enabled : previousData.tachyonEnabled, tachyonStatus: tachyon.success ? tachyon.data.status : previousData.tachyonStatus, watchdogRunning: watchdog.success ? Number(watchdog.data.running) : previousData.watchdogRunning } } }); return void 0; } // src/tachyon/fetchers/fetchHostnames.ts var callHostHints = rpc.declare({ object: "luci-rpc", method: "getHostHints", expect: { "": {} } }); async function fetchHostnames() { const hostnames = /* @__PURE__ */ new Map(); try { const hints = await callHostHints(); if (hints && typeof hints === "object") { for (const mac of Object.keys(hints)) { const hint = hints[mac]; if (hint && hint.name && hint.ipaddrs && Array.isArray(hint.ipaddrs)) { for (const ip of hint.ipaddrs) { if (!hostnames.has(ip)) { hostnames.set(ip, hint.name); } } } } } } catch (e) { console.error("fetchHostnames error", e); } return hostnames; } // src/tachyon/helpers/isActiveLuciTab.ts function isActiveLuciTab(tabId) { if (typeof document === "undefined") { return false; } return Boolean( document.querySelector( `.cbi-tab[data-tab="${tabId}"]:not(.cbi-tab-disabled)` ) ); } // src/tachyon/helpers/restoredActionLoading.ts function shouldShowLoadingForRestoredAction(state) { return state.running === true; } // src/tachyon/helpers/serviceAvailability.ts function getServiceAvailability({ loading: loading2, failed: failed2, running }) { if (loading2) { return "loading"; } if (failed2) { return "unavailable"; } return running ? "running" : "stopped"; } // src/tachyon/tabs/dashboard/initController.ts var DASHBOARD_EXPANDED_SECTIONS_KEY = "tachyon_dashboard_expanded_sections"; var expandedSections = new Set( JSON.parse(localStorage.getItem(DASHBOARD_EXPANDED_SECTIONS_KEY) || "[]") ); function toggleSectionExpanded(sectionCode) { if (expandedSections.has(sectionCode)) { expandedSections.delete(sectionCode); } else { expandedSections.add(sectionCode); } localStorage.setItem( DASHBOARD_EXPANDED_SECTIONS_KEY, JSON.stringify(Array.from(expandedSections)) ); void renderSectionsWidget(); void renderConnectionsWidget(); } var SECTIONS_REFRESH_INTERVAL_MS = 1e4; var LATENCY_TEST_BUTTON_CLASS = "dashboard-sections-grid-item-test-latency"; var LATENCY_TEST_BUTTON_LABEL_CLASS = "dashboard-sections-grid-item-test-latency__label"; var sectionsRefreshTimer = null; var sectionsRefreshPromise = null; var sectionsRefreshQueued = false; var actionStateUnsubscribe = null; var dashboardMounted = false; var dashboardMountId = 0; var dashboardDataUpdatesStarted = false; var dashboardDataUpdatesId = 0; var connectionsRefreshTimer = null; var currentConnections = []; var pageUnloading = false; var followedSubscriptionJobs = /* @__PURE__ */ new Set(); var followedLatencyJobs = /* @__PURE__ */ new Set(); var handledSubscriptionJobs = /* @__PURE__ */ new Set(); var handledLatencyJobs = /* @__PURE__ */ new Set(); var customProxyLatencies = /* @__PURE__ */ new Map(); if (typeof window !== "undefined") { window.addEventListener("pagehide", () => { pageUnloading = true; }); window.addEventListener("pageshow", () => { pageUnloading = false; }); } async function fetchDashboardSectionsOnce(mountId) { if (getDashboardServiceAvailability() === "stopped") { return false; } const prev = store.get().sectionsWidget; const hasRenderedData = prev.data.length > 0; store.set({ sectionsWidget: { ...prev, failed: false, loading: prev.loading && !hasRenderedData } }); try { const { data, success } = await CustomTachyonMethods.getDashboardSections(); if (!dashboardMounted || mountId !== dashboardMountId || getDashboardServiceAvailability() === "stopped") { return false; } if (!success) { throw new Error("failed to fetch dashboard sections"); } const current = store.get().sectionsWidget; store.set({ sectionsWidget: { ...current, loading: false, failed: false, data } }); return true; } catch (error) { logger.error("[DASHBOARD]", "fetchDashboardSections: failed", error); if (!dashboardMounted || mountId !== dashboardMountId || getDashboardServiceAvailability() === "stopped") { return false; } const current = store.get().sectionsWidget; store.set({ sectionsWidget: { ...current, loading: false, failed: current.data.length === 0, data: current.data } }); return false; } } async function fetchDashboardSections(options = {}) { if (sectionsRefreshPromise) { if (options.force) { sectionsRefreshQueued = true; } return sectionsRefreshPromise; } const mountId = dashboardMountId; const promise = (async () => { let success = false; do { sectionsRefreshQueued = false; success = await fetchDashboardSectionsOnce(mountId); } while (sectionsRefreshQueued && dashboardMounted && mountId === dashboardMountId); return success; })(); sectionsRefreshPromise = promise; try { return await promise; } finally { if (sectionsRefreshPromise === promise) { sectionsRefreshPromise = null; } } } function setSubscriptionUpdating(sectionName, updating, local = false) { if (local || !updating) { setLocalSubscriptionAction(sectionName, updating && local); } const sectionsWidget = store.get().sectionsWidget; const subscriptionUpdatingSections = { ...sectionsWidget.subscriptionUpdatingSections }; if (updating) { subscriptionUpdatingSections[sectionName] = true; } else { delete subscriptionUpdatingSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, subscriptionUpdatingSections } }); } function setSelectorSwitching(sectionName, tag) { const sectionsWidget = store.get().sectionsWidget; const selectorSwitchingSections = { ...sectionsWidget.selectorSwitchingSections }; if (tag) { selectorSwitchingSections[sectionName] = tag; } else { delete selectorSwitchingSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, selectorSwitchingSections } }); } function setLatencyFetching(sectionName, fetching, local = false, progress) { if (local || !fetching) { setLocalLatencyAction(sectionName, fetching && local); } const sectionsWidget = store.get().sectionsWidget; const latencyFetchingSections = { ...sectionsWidget.latencyFetchingSections }; const latencyProgressSections = { ...sectionsWidget.latencyProgressSections }; if (fetching) { latencyFetchingSections[sectionName] = true; if (progress) { latencyProgressSections[sectionName] = progress; } } else { delete latencyFetchingSections[sectionName]; delete latencyProgressSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, latencyFetchingSections, latencyProgressSections } }); } async function completeSubscriptionUpdateJob(jobId, sectionName, response) { if (pageUnloading) { setSubscriptionUpdating(sectionName, false); return; } if (jobId && handledSubscriptionJobs.has(jobId)) { setSubscriptionUpdating(sectionName, false); return; } const shouldNotify = jobId ? shouldNotifyOwnedUiAction("subscription", jobId) : false; const failed2 = !response.success || response.data.success === false; const message = response.success ? response.data.message || _("Failed to update subscriptions") : response.error || _("Failed to update subscriptions"); if (failed2 && isTransientRpcError(message)) { void refreshRuntimeUiState({ force: true }); return; } if (jobId) { handledSubscriptionJobs.add(jobId); } setSubscriptionUpdating(sectionName, false); if (jobId && response.success) { void TachyonShellMethods.uiActionAck("subscription", jobId); } if (failed2) { if (shouldNotify) { showToast(_("Failed to update subscriptions"), "error"); } return; } if (shouldNotify) { showToast(_("Subscriptions updated"), "success"); } void fetchDashboardSections({ force: true }); void fetchServicesInfo(); } async function followSubscriptionUpdateState(state) { const jobId = state.job_id; const sectionName = state.section || ""; if (!jobId || !sectionName || followedSubscriptionJobs.has(jobId)) { return; } if (!state.running && handledSubscriptionJobs.has(jobId)) { return; } followedSubscriptionJobs.add(jobId); if (shouldShowLoadingForRestoredAction(state)) { setSubscriptionUpdating(sectionName, true); } try { const response = state.running ? await TachyonShellMethods.waitSubscriptionUpdateJob(jobId) : { success: true, data: state }; await completeSubscriptionUpdateJob(jobId, sectionName, response); } catch (error) { logger.error("[DASHBOARD]", "followSubscriptionUpdateState failed", error); if (!pageUnloading) { const message = error instanceof Error ? error.message : _("Failed to update subscriptions"); setSubscriptionUpdating(sectionName, false); if (!isTransientRpcError(message)) { showToast(_("Failed to update subscriptions"), "error"); } } } finally { followedSubscriptionJobs.delete(jobId); } } async function completeLatencyTestJob(jobId, sectionName) { setLatencyFetching(sectionName, false); if (pageUnloading) { return; } if (jobId && handledLatencyJobs.has(jobId)) { return; } if (jobId) { handledLatencyJobs.add(jobId); } if (jobId) { void TachyonShellMethods.uiActionAck("latency", jobId); } void fetchDashboardSections({ force: true }); } async function followLatencyTestState(state) { const jobId = state.job_id; const sectionName = state.section || ""; if (!jobId || !sectionName || followedLatencyJobs.has(jobId)) { return; } if (!state.running && handledLatencyJobs.has(jobId)) { return; } followedLatencyJobs.add(jobId); if (shouldShowLoadingForRestoredAction(state)) { setLatencyFetching(sectionName, true); } try { if (state.running) { await TachyonShellMethods.waitLatencyTestJob(jobId); } await completeLatencyTestJob(jobId, sectionName); } catch (error) { logger.error("[DASHBOARD]", "followLatencyTestState failed", error); if (!pageUnloading) { setLatencyFetching(sectionName, false); } } finally { followedLatencyJobs.delete(jobId); } } function followDashboardActionsFromUiState(uiState) { for (const state of uiState.actions.subscription || []) { if (state.running || state.job_id && state.section) { void followSubscriptionUpdateState(state); } else if (state.job_id && !handledSubscriptionJobs.has(state.job_id)) { handledSubscriptionJobs.add(state.job_id); void TachyonShellMethods.uiActionAck("subscription", state.job_id); } } for (const state of uiState.actions.latency || []) { if (state.running || state.job_id && state.section) { void followLatencyTestState(state); } else if (state.job_id && !handledLatencyJobs.has(state.job_id)) { handledLatencyJobs.add(state.job_id); void TachyonShellMethods.uiActionAck("latency", state.job_id); } } } function startActionStateWatcher() { if (actionStateUnsubscribe) { return; } actionStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (dashboardMounted) { followDashboardActionsFromUiState(uiState); } }); } function stopActionStateWatcher() { if (!actionStateUnsubscribe) { return; } actionStateUnsubscribe(); actionStateUnsubscribe = null; } async function connectToClashSockets(dataUpdatesId) { const mountId = dashboardMountId; const clashApiSecret = await getClashApiSecret2(); if (!dashboardMounted || mountId !== dashboardMountId || dataUpdatesId !== dashboardDataUpdatesId || getDashboardServiceAvailability() === "stopped") { return; } socket.subscribe( `${getClashWsUrl()}/traffic?token=${clashApiSecret}`, (msg) => { if (dataUpdatesId !== dashboardDataUpdatesId || getDashboardServiceAvailability() === "stopped") { return; } const parsedMsg = JSON.parse(msg); store.set({ bandwidthWidget: { loading: false, failed: false, data: { up: parsedMsg.up, down: parsedMsg.down } } }); }, (_err) => { if (dataUpdatesId !== dashboardDataUpdatesId || getDashboardServiceAvailability() === "stopped") { return; } logger.error( "[DASHBOARD]", "connectToClashSockets - traffic: failed to connect to", getClashWsUrl() ); store.set({ bandwidthWidget: { loading: false, failed: true, data: { up: 0, down: 0 } } }); } ); socket.subscribe( `${getClashWsUrl()}/connections?token=${clashApiSecret}`, (msg) => { if (dataUpdatesId !== dashboardDataUpdatesId || getDashboardServiceAvailability() === "stopped") { return; } const parsedMsg = JSON.parse(msg); store.set({ trafficTotalWidget: { loading: false, failed: false, data: { downloadTotal: parsedMsg.downloadTotal, uploadTotal: parsedMsg.uploadTotal } }, systemInfoWidget: { loading: false, failed: false, data: { connections: parsedMsg.connections?.length, memory: parsedMsg.memory } } }); }, (_err) => { if (dataUpdatesId !== dashboardDataUpdatesId || getDashboardServiceAvailability() === "stopped") { return; } logger.error( "[DASHBOARD]", "connectToClashSockets - connections: failed to connect to", getClashWsUrl() ); store.set({ trafficTotalWidget: { loading: false, failed: true, data: { downloadTotal: 0, uploadTotal: 0 } }, systemInfoWidget: { loading: false, failed: true, data: { connections: 0, memory: 0 } } }); } ); } function getDashboardServiceAvailability() { const service = store.get().servicesInfoWidget; return getServiceAvailability({ loading: service.loading, failed: service.failed, running: service.data.tachyonRunning }); } function stopDashboardDataUpdates() { dashboardDataUpdatesStarted = false; dashboardDataUpdatesId += 1; if (sectionsRefreshTimer) { clearInterval(sectionsRefreshTimer); sectionsRefreshTimer = null; } if (connectionsRefreshTimer) { clearInterval(connectionsRefreshTimer); connectionsRefreshTimer = null; } sectionsRefreshQueued = false; socket.resetAll(); } function startDashboardDataUpdates() { if (dashboardDataUpdatesStarted || !dashboardMounted || getDashboardServiceAvailability() === "stopped") { return; } dashboardDataUpdatesStarted = true; const dataUpdatesId = ++dashboardDataUpdatesId; void fetchDashboardSections({ force: true }); void connectToClashSockets(dataUpdatesId); sectionsRefreshTimer = setInterval(() => { void fetchDashboardSections(); }, SECTIONS_REFRESH_INTERVAL_MS); void fetchConnections(); connectionsRefreshTimer = setInterval(() => { void fetchConnections(); }, SECTIONS_REFRESH_INTERVAL_MS); } function syncDashboardServiceAvailability() { const availability = getDashboardServiceAvailability(); const stopped = availability === "stopped"; const container = document.getElementById("dashboard-status"); container?.classList.toggle( "tachyon_dashboard-page--service-stopped", stopped ); if (stopped || availability === "loading") { stopDashboardDataUpdates(); return; } startDashboardDataUpdates(); } async function handleChooseOutbound(sectionName, selector, tag) { const sectionsWidget = store.get().sectionsWidget; const section = sectionsWidget.data.find( (item) => item.sectionName === sectionName ); if (!section?.withTagSelect || sectionsWidget.selectorSwitchingSections[sectionName]) { return; } setSelectorSwitching(sectionName, tag); try { const res = await TachyonShellMethods.setClashApiGroupProxy(selector, tag); if (res && !res.success && res.error) { showToast( res.message || res.error || _("Failed to switch proxy"), "error" ); } await fetchDashboardSections({ force: true }); } catch (error) { showToast(error?.message || _("Failed to switch proxy"), "error"); } finally { setSelectorSwitching(sectionName); } } function getInitialLatencyProgress(latencyType, tag) { if (latencyType !== "proxy_list") { return void 0; } try { const tags = JSON.parse(tag); if (!Array.isArray(tags)) { return void 0; } const total = tags.filter( (item) => typeof item === "string" && item.length > 0 ).length; return total > 0 ? { completed: 0, total, failed: 0 } : void 0; } catch { return void 0; } } async function handleTestLatency(latencyType, sectionName, tag, timeout) { if (store.get().sectionsWidget.latencyFetchingSections[sectionName]) { return; } setLatencyFetching( sectionName, true, true, getInitialLatencyProgress(latencyType, tag) ); let jobId = ""; let ownsJobFollow = false; let completed = false; try { if (latencyType === "proxy") { const parsedTag = tag.startsWith("[") ? JSON.parse(tag)[0] : tag; const response = await TachyonShellMethods.getClashApiProxyLatency( parsedTag, timeout ); if (response.success && response.data) { customProxyLatencies.set(tag, response.data.delay || -1); } else { customProxyLatencies.set(tag, -1); } setLatencyFetching(sectionName, false); completed = true; void fetchDashboardSections({ force: true }); } else { const startResponse = await TachyonShellMethods.latencyTestStart( latencyType, sectionName, tag, timeout ); if (!startResponse.success) { setLatencyFetching(sectionName, false); return; } jobId = startResponse.data.job_id; if (!jobId) { setLatencyFetching(sectionName, false); return; } followedLatencyJobs.add(jobId); ownsJobFollow = true; await TachyonShellMethods.waitLatencyTestJob(jobId); await completeLatencyTestJob(jobId, sectionName); completed = true; } } catch (error) { logger.error("[DASHBOARD]", "handleTestLatency: failed", error); } finally { if (ownsJobFollow) { followedLatencyJobs.delete(jobId); } if (!completed) { setLatencyFetching(sectionName, false); } } } function handleCopyOutbound(outbound) { const link = outbound.link; if (link && isCopyableProxyLink(link)) { copyToClipboard(link); return; } showToast(_("Proxy link is unavailable"), "error"); } function formatUrlTestModalValue(value) { if (typeof value === "boolean") { return value ? _("Yes") : _("No"); } const text = `${value ?? ""}`.trim(); return text || _("No"); } function getUrlTestLatencyClass(latency) { if (!latency) { return "tachyon_dashboard-page__outbound-grid__item__latency--empty"; } if (latency < 800) { return "tachyon_dashboard-page__outbound-grid__item__latency--green"; } if (latency < 1500) { return "tachyon_dashboard-page__outbound-grid__item__latency--yellow"; } return "tachyon_dashboard-page__outbound-grid__item__latency--red"; } function formatUrlTestLatency(latency) { return latency ? `${latency}ms` : "N/A"; } function renderDetailsUrl(value) { const url = `${value ?? ""}`.trim(); if (!/^https?:\/\//i.test(url)) { return E("span", {}, formatUrlTestModalValue(value)); } return E( "a", { class: "tachyon_dashboard-page__urltest-details__url", href: url, target: "_blank", rel: "noopener noreferrer" }, url ); } function getDetectedCountryFlag(country) { const code = `${country || ""}`.trim().toUpperCase(); if (!/^[A-Z]{2}$/.test(code)) { return ""; } return String.fromCodePoint( ...code.split("").map((char) => 127462 + char.charCodeAt(0) - 65) ); } function renderDetailsMemberName(member) { const countryFlag = getDetectedCountryFlag(member.country); if (!countryFlag) { return renderFlagEmojis(member.displayName); } return [ E( "span", { class: "tachyon_dashboard-page__urltest-details__country-badge" }, countryFlag ), ...renderFlagEmojis(member.displayName) ]; } function renderUrlTestSelectedValue(info) { const selectedMember = info.outbounds.find((member) => member.selected); const selectedName = selectedMember?.displayName || info.selectedName || info.selectedCode || ""; const name = formatUrlTestModalValue(selectedName); if (name === _("No")) { return E("span", {}, name); } const modeBadge = info.isManualSelection ? E( "span", { class: "badge badge-warning", style: "margin-left: 8px; padding: 2px 6px; font-size: 11px; background: rgba(255, 152, 0, 0.2); color: #ff9800; border-radius: 3px;" }, _("Manual") ) : E( "span", { class: "badge badge-info", style: "margin-left: 8px; padding: 2px 6px; font-size: 11px; background: rgba(33, 150, 243, 0.2); color: #2196f3; border-radius: 3px;" }, _("Auto") ); return E( "span", { class: "tachyon_dashboard-page__urltest-details__selected-value" }, [ E( "span", { class: "tachyon_dashboard-page__urltest-details__selected-name" }, selectedMember ? renderDetailsMemberName(selectedMember) : name ), ...selectedMember?.type ? [ E( "span", { class: "tachyon_dashboard-page__urltest-details__selected-type" }, selectedMember.type ) ] : [], ...selectedMember ? [ E( "span", { class: getUrlTestLatencyClass(selectedMember.latency) }, formatUrlTestLatency(selectedMember.latency) ) ] : [], modeBadge ] ); } function renderUrlTestCopyButton(title, onClick) { return E( "button", { type: "button", class: "btn tachyon_dashboard-page__urltest-details__copy-button", title, "aria-label": title, click: onClick }, renderCopyIcon24() ); } function renderCommonDetailsModal(info, fields, renderMemberName, isPriority, section, outbound) { const isManual = Boolean(info.isManualSelection); return E("div", { class: "tachyon_dashboard-page__urltest-details" }, [ E( "dl", { class: "tachyon_dashboard-page__urltest-details__params" }, fields.map( ({ label, value, children }) => E("div", { class: "tachyon_dashboard-page__urltest-details__param" }, [ E("dt", {}, label), E( "dd", {}, children || [E("span", {}, formatUrlTestModalValue(value))] ) ]) ) ), E("div", { class: "tachyon_dashboard-page__urltest-details__outbounds" }, [ E( "div", { class: "tachyon_dashboard-page__urltest-details__outbounds-title" }, _("Nodes") ), E( "div", { class: "tachyon_dashboard-page__urltest-details__table" }, info.outbounds.length ? info.outbounds.map((member) => { const isMemberActive = member.selected || member.code === info.selectedCode; return E( "div", { class: [ "tachyon_dashboard-page__urltest-details__row", isMemberActive ? "tachyon_dashboard-page__urltest-details__row--active" : "" ].filter(Boolean).join(" ") }, [ E( "div", { class: "tachyon_dashboard-page__urltest-details__row-name" }, isPriority ? [ E( "b", { class: "tachyon_dashboard-page__urltest-details__priority-name" }, renderMemberName(member) ), ...member.type ? [ E( "span", { class: "tachyon_dashboard-page__urltest-details__row-type" }, member.type ) ] : [] ] : [ E("b", {}, renderMemberName(member)), ...member.type ? [ E( "span", { class: "tachyon_dashboard-page__urltest-details__row-type" }, member.type ) ] : [] ] ), E( "div", { class: "tachyon_dashboard-page__urltest-details__row-actions" }, [ E( "div", { class: "tachyon_dashboard-page__urltest-details__row-meta" }, [ E( "span", { class: getUrlTestLatencyClass(member.latency) }, formatUrlTestLatency(member.latency) ) ] ), ...section && section.withTagSelect ? isMemberActive ? [ E( "span", { class: "badge badge-success tachyon_dashboard-page__urltest-details__active-badge" }, [ svgEl( "svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, [ svgEl("polyline", { points: "20 6 9 17 4 12" }) ] ), _("Active") ] ) ] : isPriority || section.outbounds.some((o) => o.code === member.code) ? [ E( "button", { type: "button", class: "btn cbi-button cbi-button-action tachyon_dashboard-page__urltest-details__select-btn", click: async (event) => { event.preventDefault(); const btn = event.currentTarget; if (btn) { btn.disabled = true; } const targetSelector = isPriority ? outbound?.code || section.code : section.code; await handleChooseOutbound( section.sectionName, targetSelector, member.code ); const updatedSection = store.get().sectionsWidget.data.find( (s) => s.sectionName === section.sectionName ); const updatedOutbound = updatedSection?.outbounds.find( (o) => o.code === outbound?.code ); if (updatedSection && updatedOutbound) { if (isPriority) { handleShowPriorityInfo( updatedSection, updatedOutbound ); } else { handleShowUrlTestInfo( updatedSection, updatedOutbound ); } } else { ui.hideModal(); } } }, _("Select") ) ] : [] : [], member.canCopyLink ? renderUrlTestCopyButton( _("Copy proxy link"), (event) => { event.preventDefault(); void handleCopyOutbound(member); } ) : E("span", { class: "tachyon_dashboard-page__urltest-details__copy-placeholder" }) ] ) ] ); }) : [ E( "div", { class: "tachyon_dashboard-page__urltest-details__empty" }, _("Node list is empty") ) ] ) ]), E("div", { class: "tachyon_dashboard-page__urltest-details__footer" }, [ ...section && outbound && isManual ? [ E( "button", { type: "button", class: "btn cbi-button cbi-button-apply", click: async (event) => { event.preventDefault(); const btn = event.currentTarget; if (btn) { btn.disabled = true; } await handleChooseOutbound( section.sectionName, section.code, outbound.code ); const updatedSection = store.get().sectionsWidget.data.find( (s) => s.sectionName === section.sectionName ); const updatedOutbound = updatedSection?.outbounds.find( (o) => o.code === outbound.code ); if (updatedSection && updatedOutbound) { if (isPriority) { handleShowPriorityInfo(updatedSection, updatedOutbound); } else { handleShowUrlTestInfo(updatedSection, updatedOutbound); } } else { ui.hideModal(); } } }, _("Switch to auto-selection") ) ] : [], E( "button", { type: "button", class: "btn cbi-button cbi-button-neutral", click: () => { ui.hideModal(); } }, _("Close") ) ]) ]); } function renderUrlTestInfoModal(outbound, section) { const info = outbound.urlTestInfo; if (!info) { return E("div", {}, _("URLTest details are unavailable")); } const fields = [ { label: _("Selected"), children: [renderUrlTestSelectedValue(info)] }, { label: _("Testing URL"), children: [renderDetailsUrl(info.url)] }, { label: _("Interval"), value: info.interval }, { label: _("Tolerance"), value: info.tolerance }, { label: _("Idle timeout"), value: info.idleTimeout }, { label: _("Interrupt connections"), value: info.interruptExistConnections } ]; return renderCommonDetailsModal( info, fields, (member) => renderDetailsMemberName(member), false, section, outbound ); } function handleShowUrlTestInfo(section, outbound) { if (!outbound.urlTestInfo) { return; } ui.showModal( `${_("URLTest details")}: ${outbound.urlTestInfo.displayName || outbound.displayName}`, renderUrlTestInfoModal(outbound, section) ); } function renderPrioritySelectedValue(info) { const selectedMember = info.outbounds.find((member) => member.selected); const selectedName = selectedMember?.displayName || info.selectedName || info.selectedCode || ""; const name = formatUrlTestModalValue(selectedName); if (name === _("No")) { return E("span", {}, name); } const modeBadge = info.isManualSelection ? E( "span", { class: "badge badge-warning", style: "margin-left: 8px; padding: 2px 6px; font-size: 11px; background: rgba(255, 152, 0, 0.2); color: #ff9800; border-radius: 3px;" }, _("Manual") ) : E( "span", { class: "badge badge-info", style: "margin-left: 8px; padding: 2px 6px; font-size: 11px; background: rgba(33, 150, 243, 0.2); color: #2196f3; border-radius: 3px;" }, _("Auto") ); return E( "span", { class: "tachyon_dashboard-page__urltest-details__selected-value" }, [ E( "span", { class: [ "tachyon_dashboard-page__urltest-details__selected-name", selectedMember ? "tachyon_dashboard-page__urltest-details__priority-name" : "" ].filter(Boolean).join(" ") }, selectedMember ? renderPriorityMemberName(selectedMember) : name ), ...selectedMember?.type ? [ E( "span", { class: "tachyon_dashboard-page__urltest-details__selected-type" }, selectedMember.type ) ] : [], ...selectedMember ? [ E( "span", { class: getUrlTestLatencyClass(selectedMember.latency) }, formatUrlTestLatency(selectedMember.latency) ) ] : [], modeBadge ] ); } function renderPriorityMemberName(member) { const countryFlag = getDetectedCountryFlag(member.country); const flagElements = countryFlag ? [ E( "span", { class: "tachyon_dashboard-page__urltest-details__country-badge" }, countryFlag ) ] : []; return [ ...flagElements, E( "span", { class: "tachyon_dashboard-page__urltest-details__priority-level" }, `[${member.levelName}]` ), ...renderFlagEmojis(member.displayName) ]; } function renderPriorityInfoModal(outbound, section) { const info = outbound.priorityInfo; if (!info) { return E("div", {}, _("Priority details are unavailable")); } const fields = [ { label: _("Selected"), children: [renderPrioritySelectedValue(info)] }, { label: _("Check URL"), children: [renderDetailsUrl(info.healthUrl)] }, { label: _("Check interval"), value: info.activeCheckInterval }, { label: _("Unavailability timeout"), value: info.checkTimeout }, { label: _("Higher-level check interval"), value: info.recoveryCheckInterval }, { label: _("Select the fastest node"), value: info.pickFastest }, { label: _("Automatically select the fastest node in the current level"), value: info.switchToFasterSamePriority }, ...info.switchToFasterSamePriority ? [ { label: _("Faster server search interval"), value: info.fastestCheckInterval } ] : [], { label: _("Interrupt connections"), value: info.interruptExistConnections } ]; return renderCommonDetailsModal( info, fields, (member) => renderPriorityMemberName(member), true, section, outbound ); } function handleShowPriorityInfo(section, outbound) { if (!outbound.priorityInfo) { return; } ui.showModal( `${_("Priority details")}: ${outbound.priorityInfo.displayName || outbound.displayName}`, renderPriorityInfoModal(outbound, section) ); } async function handleUpdateSubscription(section) { if (store.get().sectionsWidget.subscriptionUpdatingSections[section.sectionName]) { return; } setSubscriptionUpdating(section.sectionName, true, true); let jobId = ""; let ownsJobFollow = false; try { const startResponse = await TachyonShellMethods.subscriptionUpdateStart( section.sectionName ); if (!startResponse.success) { throw new Error(startResponse.error); } jobId = startResponse.data.job_id; markUiActionOwned("subscription", jobId); if (followedSubscriptionJobs.has(jobId)) { return; } followedSubscriptionJobs.add(jobId); ownsJobFollow = true; const response = await TachyonShellMethods.waitSubscriptionUpdateJob(jobId); await completeSubscriptionUpdateJob(jobId, section.sectionName, response); } catch (error) { logger.error("[DASHBOARD]", "handleUpdateSubscription: failed", error); if (!pageUnloading) { const message = error instanceof Error ? error.message : _("Failed to update subscriptions"); setSubscriptionUpdating(section.sectionName, false); if (!isTransientRpcError(message)) { showToast(_("Failed to update subscriptions"), "error"); } } } finally { if (ownsJobFollow) { followedSubscriptionJobs.delete(jobId); } } } function shallowRecordEqual(left, right) { const leftKeys = Object.keys(left); const rightKeys = Object.keys(right); if (leftKeys.length !== rightKeys.length) { return false; } return leftKeys.every((key) => left[key] === right[key]); } function canUpdateLatencyProgressInline(prev, next) { return prev.loading === next.loading && prev.failed === next.failed && prev.data === next.data && shallowRecordEqual( prev.latencyFetchingSections, next.latencyFetchingSections ) && shallowRecordEqual( prev.subscriptionUpdatingSections, next.subscriptionUpdatingSections ) && shallowRecordEqual( prev.selectorSwitchingSections, next.selectorSwitchingSections ); } function findLatencyTestButton(container, sectionName) { return Array.from( container.querySelectorAll( `.${LATENCY_TEST_BUTTON_CLASS}` ) ).find((button) => button.dataset.latencySection === sectionName); } function updateLatencyProgressInline(sectionsWidget) { const container = document.getElementById("dashboard-sections-grid"); if (!container) { return false; } for (const section of sectionsWidget.data) { if (!sectionsWidget.latencyFetchingSections[section.sectionName]) { continue; } const button = findLatencyTestButton(container, section.sectionName); const label = button?.querySelector( `.${LATENCY_TEST_BUTTON_LABEL_CLASS}` ); if (!label) { return false; } const isConnectionNode = ["vpn", "awg", "warp"].includes( section.action || "" ); const text = isConnectionNode ? _("Checking Connection...") : getLatencyTestLabel( sectionsWidget.latencyProgressSections[section.sectionName] ); if (label.textContent !== text) { label.textContent = text; } } return true; } async function renderSectionsWidget() { logger.debug("[DASHBOARD]", "renderSectionsWidget"); const sectionsWidget = store.get().sectionsWidget; const container = document.getElementById("dashboard-sections-grid"); if (!container) { return; } const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({ ...section, outbounds: section.outbounds.map((outbound) => ({ ...outbound, latency: customProxyLatencies.has(outbound.code) ? customProxyLatencies.get(outbound.code) : outbound.latency })) })); if (sectionsWidget.loading || sectionsWidget.failed) { const renderedWidget = renderSections({ loading: sectionsWidget.loading, failed: sectionsWidget.failed, section: { code: "", sectionName: "", displayName: "", outbounds: [], withTagSelect: false }, isCollapsed: false, onToggleCollapse: () => { }, onTestLatency: () => { }, onChooseOutbound: () => { }, onCopyOutbound: () => { }, onShowUrlTestInfo: () => { }, onShowPriorityInfo: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, latencyProgress: void 0, subscriptionUpdating: false, selectorSwitchingTag: void 0 }); return preserveScrollForPage(() => { container.replaceChildren(renderedWidget); }); } const renderedWidgets = sectionsWithCustomLatencies.map( (section) => renderSections({ loading: sectionsWidget.loading, failed: sectionsWidget.failed, section, isCollapsed: !expandedSections.has(section.code), onToggleCollapse: () => toggleSectionExpanded(section.code), latencyFetching: Boolean( sectionsWidget.latencyFetchingSections[section.sectionName] ), latencyProgress: sectionsWidget.latencyProgressSections[section.sectionName], subscriptionUpdating: Boolean( sectionsWidget.subscriptionUpdatingSections[section.sectionName] ), selectorSwitchingTag: sectionsWidget.selectorSwitchingSections[section.sectionName], onTestLatency: (tag) => { if (section.withTagSelect) { if (Array.isArray(tag)) { return handleTestLatency( "proxy_list", section.sectionName, JSON.stringify(tag) ); } return handleTestLatency("group", section.sectionName, tag); } return handleTestLatency( "proxy", section.sectionName, Array.isArray(tag) ? JSON.stringify(tag) : tag, section.latencyTestTimeout ); }, onChooseOutbound: (sectionName, selector, tag) => { void handleChooseOutbound(sectionName, selector, tag); }, onCopyOutbound: (_section, outbound) => { handleCopyOutbound(outbound); }, onShowUrlTestInfo: (section2, outbound) => { handleShowUrlTestInfo(section2, outbound); }, onShowPriorityInfo: (section2, outbound) => { handleShowPriorityInfo(section2, outbound); }, onUpdateSubscription: (section2) => { void handleUpdateSubscription(section2); } }) ); return preserveScrollForPage(() => { container.replaceChildren(...renderedWidgets); }); } function renderStoreWidget(containerId, storeKey, title, getItems, debugName) { logger.debug("[DASHBOARD]", debugName); const widgetState = store.get()[storeKey]; const container = document.getElementById(containerId); if (!container) return; if (widgetState.loading || widgetState.failed) { const renderedWidget2 = renderWidget({ loading: widgetState.loading, failed: widgetState.failed, title: "", items: [] }); return container.replaceChildren(renderedWidget2); } const renderedWidget = renderWidget({ loading: widgetState.loading, failed: widgetState.failed, title, items: getItems(widgetState.data) }); container.replaceChildren(renderedWidget); } async function fetchConnections() { try { const [res, hostnames] = await Promise.all([ TachyonShellMethods.getClashApiConnections(), fetchHostnames() ]); if (res.success && res.data && typeof res.data === "object" && Array.isArray(res.data.connections)) { const connectionsList = res.data.connections; const map = /* @__PURE__ */ new Map(); for (const conn of connectionsList) { const ip = conn.metadata?.sourceIP; if (!ip) continue; const up = Number(conn.upload) || 0; const down = Number(conn.download) || 0; if (map.has(ip)) { const existing = map.get(ip); existing.count++; existing.upload += up; existing.download += down; } else { const name = hostnames.get(ip); map.set(ip, { ip, count: 1, upload: up, download: down, name }); } } currentConnections = Array.from(map.values()).sort( (a, b) => b.download + b.upload - (a.download + a.upload) ); } } catch (_e) { } renderConnectionsWidget(); } function renderConnectionsWidget() { const container = document.getElementById("dashboard-connections-grid"); if (!container) return; container.replaceChildren( renderConnections( currentConnections, !expandedSections.has("active_clients"), () => toggleSectionExpanded("active_clients") ) ); } async function renderBandwidthWidget() { renderStoreWidget( "dashboard-widget-traffic", "bandwidthWidget", _("Traffic"), (data) => [ { key: _("Uplink"), value: `${prettyBytes(data.up)}/s` }, { key: _("Downlink"), value: `${prettyBytes(data.down)}/s` } ], "renderBandwidthWidget" ); } async function renderTrafficTotalWidget() { renderStoreWidget( "dashboard-widget-traffic-total", "trafficTotalWidget", _("Traffic Total"), (data) => [ { key: _("Uplink"), value: String(prettyBytes(data.uploadTotal)) }, { key: _("Downlink"), value: String(prettyBytes(data.downloadTotal)) } ], "renderTrafficTotalWidget" ); } async function renderSystemInfoWidget() { renderStoreWidget( "dashboard-widget-system-info", "systemInfoWidget", _("System info"), (data) => [ { key: _("Active Connections"), value: String(data.connections) }, { key: _("Memory Usage"), value: String(prettyBytes(data.memory)) } ], "renderSystemInfoWidget" ); } async function renderServicesInfoWidget() { renderStoreWidget( "dashboard-widget-service-info", "servicesInfoWidget", _("Services info"), (data) => [ { key: "Tachyon", value: data.tachyonRunning ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: data.tachyonRunning ? "tachyon_dashboard-page__widgets-section__item__row--success" : "tachyon_dashboard-page__widgets-section__item__row--error" } }, { key: "Sing-box", value: data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: data.singbox ? "tachyon_dashboard-page__widgets-section__item__row--success" : "tachyon_dashboard-page__widgets-section__item__row--error" } } ], "renderServicesInfoWidget" ); } async function onStoreUpdate(next, prev, diff) { if (diff.sectionsWidget) { const inlineUpdated = canUpdateLatencyProgressInline( prev.sectionsWidget, next.sectionsWidget ) && updateLatencyProgressInline(next.sectionsWidget); if (!inlineUpdated) { renderSectionsWidget(); } } if (diff.bandwidthWidget) { renderBandwidthWidget(); } if (diff.trafficTotalWidget) { renderTrafficTotalWidget(); } if (diff.systemInfoWidget) { renderSystemInfoWidget(); } if (diff.servicesInfoWidget) { syncDashboardServiceAvailability(); renderServicesInfoWidget(); } } async function onPageMount() { onPageUnmount(); dashboardMounted = true; dashboardMountId += 1; const mountId = dashboardMountId; const hasRuntimeSnapshot = Boolean(getCachedRuntimeUiState()); if (!hasRuntimeSnapshot) { const uiState = await refreshRuntimeUiState({ force: true }); if (!dashboardMounted || mountId !== dashboardMountId) { return; } if (!uiState) { void fetchServicesInfo(); } } store.subscribe(onStoreUpdate); startActionStateWatcher(); void renderSectionsWidget(); void renderConnectionsWidget(); void renderBandwidthWidget(); void renderTrafficTotalWidget(); void renderSystemInfoWidget(); void renderServicesInfoWidget(); syncDashboardServiceAvailability(); if (hasRuntimeSnapshot) { void refreshRuntimeUiState({ force: true }); } } function onPageUnmount() { dashboardMounted = false; dashboardMountId += 1; stopDashboardDataUpdates(); stopActionStateWatcher(); sectionsRefreshQueued = false; sectionsRefreshPromise = null; store.unsubscribe(onStoreUpdate); store.reset(["bandwidthWidget", "trafficTotalWidget", "systemInfoWidget"]); } var dashboardLifecycleRegistered = false; var dashboardControllerInitialized = false; function registerLifecycleListeners() { if (dashboardLifecycleRegistered) { return; } dashboardLifecycleRegistered = true; store.subscribe((next, prev, diff) => { if (diff.tabService && next.tabService.current !== prev.tabService.current) { logger.debug( "[DASHBOARD]", "active tab diff event, active tab:", diff.tabService.current ); const isDashboardVisible = next.tabService.current === "dashboard"; if (isDashboardVisible) { logger.debug( "[DASHBOARD]", "registerLifecycleListeners", "onPageMount" ); return onPageMount(); } if (!isDashboardVisible) { logger.debug( "[DASHBOARD]", "registerLifecycleListeners", "onPageUnmount" ); return onPageUnmount(); } } }); } async function initController() { if (dashboardControllerInitialized) { return; } dashboardControllerInitialized = true; onMount("dashboard-status").then(() => { logger.debug("[DASHBOARD]", "initController", "onMount"); registerLifecycleListeners(); if (store.get().tabService.current === "dashboard" || isActiveLuciTab("dashboard")) { onPageMount(); } }); } // src/tachyon/tabs/dashboard/styles.ts var styles = ` @font-face { font-family: "Twemoji Country Flags"; src: url("/luci-static/resources/view/tachyon/fonts/TwemojiCountryFlags.woff2") format("woff2"); font-display: swap; font-style: normal; font-weight: normal; unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F; } #cbi-${TACHYON_UCI_PACKAGE}-dashboard-_mount_node > .cbi-value-title { display: none; } #cbi-${TACHYON_UCI_PACKAGE}-dashboard-_mount_node > .cbi-value-field { margin-left: 0; width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-dashboard-_mount_node > div { width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-dashboard > h3 { display: none; } .tachyon_dashboard-page { display: table; table-layout: fixed; width: 100%; --dashboard-grid-columns: 4; --dashboard-grid-min-width: 180px; } .tachyon_dashboard-page__service-stopped { display: none; width: 100%; min-height: 180px; margin-top: 10px; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; border: 1px dashed var(--border-color-high, #555); border-radius: 6px; color: var(--text-color-medium, #888); background: transparent; font-family: inherit; font-size: inherit; font-weight: inherit; line-height: inherit; font-style: italic; text-align: center; } .tachyon_dashboard-page--service-stopped { display: grid; grid-template-columns: repeat(var(--dashboard-grid-columns), minmax(var(--dashboard-grid-min-width), 1fr)); gap: 10px; } .tachyon_dashboard-page--service-stopped .tachyon_dashboard-page__service-stopped { display: flex; grid-column: 1 / -1; } .tachyon_dashboard-page--service-stopped .tachyon_dashboard-page__content { display: none; } @media (max-width: 900px) { .tachyon_dashboard-page { --dashboard-grid-columns: 2; } } @media (max-width: 560px) { .tachyon_dashboard-page { --dashboard-grid-columns: 1; --dashboard-grid-min-width: 0; } } .tachyon_dashboard-page__widgets-section { margin-top: 10px; display: grid; grid-template-columns: repeat(var(--dashboard-grid-columns), minmax(var(--dashboard-grid-min-width), 1fr)); grid-gap: 10px; } .tachyon_dashboard-page__widgets-section__item { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; min-width: 0; } .tachyon_dashboard-page__widgets-section__item__title {} .tachyon_dashboard-page__widgets-section__item__row {} .tachyon_dashboard-page__widgets-section__item__row--success .tachyon_dashboard-page__widgets-section__item__row__value { color: var(--success-color-medium, green); } .tachyon_dashboard-page__widgets-section__item__row--error .tachyon_dashboard-page__widgets-section__item__row__value { color: var(--error-color-medium, red); } .tachyon_dashboard-page__widgets-section__item__row__key {} .tachyon_dashboard-page__widgets-section__item__row__value {} .tachyon_dashboard-page__outbound-section { margin-top: 10px; border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; width: 100%; box-sizing: border-box; } .tachyon_dashboard-page__outbound-section__title-section { display: flex; align-items: center; justify-content: space-between; gap: 8px 10px; min-width: 0; } .tachyon_dashboard-page__outbound-section__title-section__title { color: var(--text-color-high); font-weight: 700; min-width: 0; overflow-wrap: anywhere; } .tachyon_dashboard-page__outbound-section__title-section__actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex: 0 0 auto; } .tachyon_dashboard-page .btn.tachyon_dashboard-page__outbound-section__subscription-update { min-width: 130px; min-height: 28px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; } .tachyon_dashboard-page__outbound-section__subscription-update svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .tachyon_dashboard-page__outbound-section__subscription-update[disabled] { cursor: not-allowed; opacity: 0.65; } .tachyon_dashboard-page .btn.dashboard-sections-grid-item-test-latency { min-width: 99px; min-height: 28px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; } .tachyon_dashboard-page .btn.dashboard-sections-grid-item-test-latency svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .tachyon_dashboard-page .btn.dashboard-sections-grid-item-test-latency[disabled] { cursor: not-allowed; opacity: 0.65; } .tachyon_dashboard-page__outbound-grid { margin-top: 5px; display: grid; grid-template-columns: repeat(var(--dashboard-grid-columns), minmax(var(--dashboard-grid-min-width), 1fr)); grid-gap: 10px; } .tachyon_dashboard-page__subscription-meta { --subscription-meta-action-size: 28px; --subscription-meta-action-gap: 6px; grid-column: 1 / -1; border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 8px 10px; background: var(--background-color-high, transparent); } .tachyon_dashboard-page__subscription-meta__main { display: flex; align-items: center; gap: 6px 10px; min-width: 0; } .tachyon_dashboard-page__subscription-meta__heading { flex: 0 0 auto; color: var(--text-color-high); font-weight: 700; line-height: 1.25; white-space: nowrap; } .tachyon_dashboard-page__subscription-meta__title { flex: 0 1 auto; color: var(--text-color-high); font-weight: 700; line-height: 1.25; word-break: break-word; } .tachyon_dashboard-page__subscription-meta__facts { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 5px 12px; } .tachyon_dashboard-page__subscription-meta__fact { display: flex; align-items: baseline; gap: 4px; min-width: 0; line-height: 1.25; } .tachyon_dashboard-page__subscription-meta__fact-key { color: var(--text-color-medium); font-size: 12px; white-space: nowrap; } .tachyon_dashboard-page__subscription-meta__fact-value { color: var(--text-color-high); font-weight: 600; overflow-wrap: anywhere; } .tachyon_dashboard-page__subscription-meta__actions { flex: 0 0 auto; margin-left: auto; display: flex; justify-content: flex-end; gap: var(--subscription-meta-action-gap); } .tachyon_dashboard-page .btn.tachyon_dashboard-page__subscription-meta__action { width: var(--subscription-meta-action-size); height: var(--subscription-meta-action-size); min-width: var(--subscription-meta-action-size); min-height: var(--subscription-meta-action-size); padding: 2px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; line-height: 1; margin: 0; } .tachyon_dashboard-page__subscription-meta__action svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .tachyon_dashboard-page__subscription-meta__announce { margin: 6px 0 0; border-left: 3px solid var(--primary-color-medium, dodgerblue); padding: 4px 8px; background: var(--background-color-low, rgba(0, 0, 0, 0.04)); color: var(--text-color-medium); font-style: italic; line-height: 1.25; overflow-wrap: anywhere; } @media (max-width: 700px) { .tachyon_dashboard-page__subscription-meta__main { align-items: flex-start; flex-wrap: wrap; } .tachyon_dashboard-page__subscription-meta__heading, .tachyon_dashboard-page__subscription-meta__title { order: 1; } .tachyon_dashboard-page__subscription-meta__actions { order: 2; } .tachyon_dashboard-page__subscription-meta__facts { order: 3; flex-basis: 100%; } .tachyon_dashboard-page__subscription-meta__title { max-width: calc(100% - 42px); } } .tachyon_dashboard-page__outbound-grid__item { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; transition: border 0.2s ease; min-width: 0; position: relative; } .tachyon_dashboard-page__outbound-grid__item--selectable { cursor: pointer; } .tachyon_dashboard-page__outbound-grid__item--selectable:hover { border-color: var(--primary-color-high, dodgerblue); } .tachyon_dashboard-page__outbound-grid__item--active { border-color: var(--success-color-medium, green); } .tachyon_dashboard-page__outbound-grid__item--disabled { cursor: default; } .tachyon_dashboard-page__outbound-grid__item--switching { border-color: transparent !important; overflow: hidden; cursor: wait; } .tachyon_dashboard-page__outbound-grid__item__snake { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999; box-sizing: border-box; } .tachyon_dashboard-page__outbound-grid__item__snake rect { stroke: var(--primary-color-high, dodgerblue); stroke-width: 4; animation: tachyon-dashboard-selector-snake-svg 1.2s linear infinite; } @keyframes tachyon-dashboard-selector-snake-svg { 0% { stroke-dasharray: 30 70; stroke-dashoffset: 100; } 100% { stroke-dasharray: 30 70; stroke-dashoffset: 0; } } .tachyon_dashboard-page__outbound-grid__item__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; min-width: 0; } .tachyon_dashboard-page__outbound-grid__item__header b { min-width: 0; line-height: 1.25; overflow-wrap: anywhere; } .tachyon_dashboard-page .btn.tachyon_dashboard-page__outbound-grid__item__copy-button { width: 22px; height: 22px; min-width: 22px; min-height: 22px; padding: 1px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; line-height: 1; } .tachyon_dashboard-page__outbound-grid__item__copy-button svg { width: 13px; height: 13px; display: block; flex: 0 0 auto; } .tachyon_dashboard-page__outbound-grid__item__footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; min-width: 0; } .tachyon_dashboard-page__outbound-grid__item__type { min-width: 0; overflow-wrap: anywhere; } .tachyon_dashboard-page__outbound-grid__item__latency--empty { color: var(--primary-color-low, lightgray); } .tachyon_dashboard-page__outbound-grid__item__latency--green { color: var(--success-color-medium, green); } .tachyon_dashboard-page__outbound-grid__item__latency--yellow { color: var(--warn-color-medium, orange); } .tachyon_dashboard-page__outbound-grid__item__latency--red { color: var(--error-color-medium, red); } .tachyon_dashboard-page__urltest-details { box-sizing: border-box; width: min(760px, calc(100vw - 56px)); max-width: 100%; padding-top: 10px; } .tachyon_dashboard-page__urltest-details__params { display: grid; grid-template-columns: minmax(120px, max-content) minmax(0, 1fr); gap: 8px 16px; margin: 0 0 18px; } .tachyon_dashboard-page__urltest-details__param { display: contents; } .tachyon_dashboard-page__urltest-details__param dt { color: var(--text-color-medium, #666); line-height: 1.35; } .tachyon_dashboard-page__urltest-details__param dd { display: flex; align-items: center; gap: 8px; min-width: 0; margin: 0; } .tachyon_dashboard-page__urltest-details__param dd span { min-width: 0; overflow-wrap: anywhere; } .tachyon_dashboard-page__urltest-details__url { min-width: 0; color: var(--primary-color-high, #337ab7); text-decoration: none; overflow-wrap: anywhere; } .tachyon_dashboard-page__urltest-details__url:hover { text-decoration: underline; } .tachyon_dashboard-page__urltest-details__selected-value { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; max-width: 100%; padding: 0; border: 0; color: inherit; background: transparent; box-sizing: border-box; line-height: 1.3; } .tachyon_dashboard-page__urltest-details__selected-name { min-width: 0; font-weight: 600; overflow-wrap: anywhere; } .tachyon_dashboard-page__urltest-details__selected-type { color: var(--text-color-medium, #666); } .tachyon_dashboard-page__urltest-details__outbounds-title { margin-bottom: 8px; font-weight: 600; } .tachyon_dashboard-page__urltest-details__table { display: flex; flex-direction: column; gap: 6px; width: 100%; box-sizing: border-box; max-height: min(48vh, 480px); overflow-x: hidden; overflow-y: auto; padding-right: 4px; } .tachyon_dashboard-page__urltest-details__row { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-width: 0; padding: 8px 10px; box-sizing: border-box; border: 1px solid var(--border-color-low, rgba(128, 128, 128, 0.15)); border-radius: 6px; background: var(--background-color-card, rgba(128, 128, 128, 0.04)); transition: background 0.15s ease, border-color 0.15s ease; } .tachyon_dashboard-page__urltest-details__row:hover { background: rgba(128, 128, 128, 0.08); } .tachyon_dashboard-page__urltest-details__row--active { border-color: #4caf50; background: rgba(76, 175, 80, 0.08); } .tachyon_dashboard-page__urltest-details__row-name { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; overflow: hidden; } .tachyon_dashboard-page__urltest-details__row-name b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 1.3; } .tachyon_dashboard-page__urltest-details__priority-name { display: inline-flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tachyon_dashboard-page__urltest-details__priority-number { margin-right: 4px; color: var(--text-color-medium, #aaa); font-family: monospace; font-size: 12px; font-weight: 600; } .tachyon_dashboard-page__urltest-details__priority-level { padding: 1px 6px; border-radius: 4px; color: var(--text-color-medium, #aaa); background: rgba(128, 128, 128, 0.15); font-size: 11px; font-weight: 400; white-space: nowrap; } .tachyon_dashboard-page__urltest-details__country-badge { display: inline-flex; align-items: center; user-select: none; padding: 2px 4px; border: 1px solid rgba(128, 128, 128, 0.25); border-radius: 4px; background: rgba(128, 128, 128, 0.15); line-height: 1; flex-shrink: 0; } .tachyon_dashboard-page__flag-emoji, .tachyon_dashboard-page__urltest-details__country-badge { font-family: "Twemoji Country Flags"; font-style: normal; font-weight: normal; } .tachyon_dashboard-page__urltest-details__priority-node { color: var(--text-color-high, #fff); font-weight: 600; } .tachyon_dashboard-page__urltest-details__row-type { font-size: 11px; color: var(--text-color-medium, #888); background: rgba(128, 128, 128, 0.12); padding: 1px 6px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; } .tachyon_dashboard-page__urltest-details__row-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .tachyon_dashboard-page__urltest-details__row-meta { display: flex; align-items: center; white-space: nowrap; min-width: 48px; justify-content: flex-end; font-weight: 500; font-size: 12px; } .tachyon_dashboard-page__urltest-details__select-btn { padding: 2px 10px !important; font-size: 12px !important; height: 26px !important; line-height: 20px !important; white-space: nowrap !important; border-radius: 4px !important; cursor: pointer; } .tachyon_dashboard-page__urltest-details__active-badge { display: inline-flex !important; align-items: center !important; gap: 4px !important; padding: 3px 8px !important; font-size: 11px !important; font-weight: 600 !important; color: #4caf50 !important; background: rgba(76, 175, 80, 0.15) !important; border: 1px solid rgba(76, 175, 80, 0.3) !important; border-radius: 4px !important; white-space: nowrap !important; } .tachyon_dashboard-page__urltest-details__copy-button { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 24px; width: 24px; min-width: 24px; height: 24px; padding: 0; box-sizing: border-box; border-radius: 4px; opacity: 0.7; transition: opacity 0.15s ease; } .tachyon_dashboard-page__urltest-details__copy-button:hover { opacity: 1; } .tachyon_dashboard-page__urltest-details__copy-button svg { width: 14px; height: 14px; } .tachyon_dashboard-page__urltest-details__copy-placeholder { display: block; width: 24px; min-width: 24px; height: 1px; } .tachyon_dashboard-page__urltest-details__empty { margin-top: 4px; padding: 24px 0; border: 1px dashed var(--border-color-high, #555); border-radius: 4px; color: var(--text-color-medium, #888); background: rgba(128, 128, 128, 0.02); font-style: italic; text-align: center; } .tachyon_dashboard-page__urltest-details__footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border-color-low, rgba(128, 128, 128, 0.2)); } @media (max-width: 560px) { .tachyon_dashboard-page__urltest-details__params { grid-template-columns: 1fr; } .tachyon_dashboard-page__urltest-details__row { flex-wrap: wrap; gap: 6px; } .tachyon_dashboard-page__urltest-details__row-actions { width: 100%; justify-content: flex-end; } } `; // src/tachyon/tabs/dashboard/index.ts var DashboardTab = { render, initController, styles }; // src/tachyon/tabs/diagnostic/renderDiagnostic.ts function render2() { return E( "div", { id: "diagnostic-status", class: "tachyon_diagnostic-page" }, [ E("div", { class: "tachyon_diagnostic-page__left-bar" }, [ E("div", { id: "tachyon_diagnostic-page-run-check" }), E("div", { class: "tachyon_diagnostic-page__checks", id: "tachyon_diagnostic-page-checks" }) ]), E("div", { class: "tachyon_diagnostic-page__right-bar" }, [ E("div", { id: "tachyon_diagnostic-page-wiki" }), E("div", { id: "tachyon_diagnostic-page-actions" }), E("div", { id: "tachyon_diagnostic-page-system-info" }) ]) ] ); } // src/tachyon/tabs/diagnostic/checks/updateCheckStore.ts function updateCheckStore(check, minified) { const diagnosticsChecks = store.get().diagnosticsChecks; const other = diagnosticsChecks.filter((item) => item.code !== check.code); const smallCheck = { ...check, items: check.items.filter((item) => item.state !== "success") }; const targetCheck = minified ? smallCheck : check; store.set({ diagnosticsChecks: [...other, targetCheck] }); } // src/tachyon/tabs/diagnostic/helpers/getMeta.ts function getMeta({ allGood, atLeastOneGood }) { if (allGood) { return { state: "success", description: _("Checks passed") }; } if (atLeastOneGood) { return { state: "warning", description: _("Issues detected") }; } return { state: "error", description: _("Checks failed") }; } // src/tachyon/tabs/diagnostic/checks/getDnsCheckPresentation.ts function getDnsCheckPresentation(data) { const dhcpManagedManually = Boolean(data.dont_touch_dhcp); const dhcpCheckOk = dhcpManagedManually || Boolean(data.dhcp_config_status); const allGood = Boolean(data.dns_on_router) && dhcpCheckOk && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status); const atLeastOneGood = Boolean(data.dns_on_router) || dhcpCheckOk || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status); const meta = getMeta({ atLeastOneGood, allGood }); const state = dhcpManagedManually && meta.state === "success" ? "warning" : meta.state; const description = dhcpManagedManually && meta.state === "success" ? _("Checks passed with manual DHCP") : meta.description; const dhcpItemState = dhcpManagedManually ? "warning" : data.dhcp_config_status ? "success" : "error"; const dhcpItemKey = dhcpManagedManually ? _("DHCP is managed manually") : _("DHCP has DNS server"); return { state, description, dhcpItemState, dhcpItemKey }; } // src/tachyon/tabs/diagnostic/checks/runDnsCheck.ts async function runDnsCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const dnsChecks = await TachyonShellMethods.checkDNSAvailable(); if (!dnsChecks.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("DNS checks failed"); } const data = dnsChecks.data; const { state, description, dhcpItemState, dhcpItemKey } = getDnsCheckPresentation(data); updateCheckStore({ order, code, title, description, state, items: [ ...insertIf( data.dns_type === "doh" || data.dns_type === "dot" || data.bootstrap_dns_server_count > 1 || !data.bootstrap_dns_status, [ { state: data.bootstrap_dns_status ? "success" : "error", key: data.bootstrap_dns_server_count > 1 ? _("Active Bootstrap DNS") : _("Bootstrap DNS"), value: data.bootstrap_dns_server_count > 1 ? `${data.bootstrap_dns_server} (${data.bootstrap_dns_server_index + 1}/${data.bootstrap_dns_server_count})` : data.bootstrap_dns_server } ] ), { state: data.dns_status ? "success" : "error", key: data.dns_server_count > 1 ? _("Active Main DNS") : _("Main DNS"), value: data.dns_server_count > 1 ? `${data.dns_server} [${data.dns_type}] (${data.dns_server_index + 1}/${data.dns_server_count})` : `${data.dns_server} [${data.dns_type}]` }, { state: data.dns_on_router ? "success" : "error", key: _("DNS on router"), value: "" }, { state: dhcpItemState, key: dhcpItemKey, value: "" } ] }); if (state === "error") { throw new Error("DNS checks failed"); } } // src/tachyon/tabs/diagnostic/checks/runSingBoxCheck.ts async function runSingBoxCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const singBoxChecks = await TachyonShellMethods.checkSingBox(); if (!singBoxChecks.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Sing-box checks failed"); } const data = singBoxChecks.data; const allGood = Boolean(data.sing_box_installed) && Boolean(data.sing_box_version_ok) && Boolean(data.sing_box_service_exist) && Boolean(data.sing_box_autostart_disabled) && Boolean(data.sing_box_process_running) && Boolean(data.sing_box_ports_listening); const atLeastOneGood = Boolean(data.sing_box_installed) || Boolean(data.sing_box_version_ok) || Boolean(data.sing_box_service_exist) || Boolean(data.sing_box_autostart_disabled) || Boolean(data.sing_box_process_running) || Boolean(data.sing_box_ports_listening); const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, code, title, description, state, items: [ { state: data.sing_box_installed ? "success" : "error", key: _("Sing-box installed"), value: "" }, { state: data.sing_box_version_ok ? "success" : "error", key: _("Sing-box version is compatible (newer than 1.12.4)"), value: "" }, { state: data.sing_box_service_exist ? "success" : "error", key: _("Sing-box service exist"), value: "" }, { state: data.sing_box_autostart_disabled ? "success" : "error", key: _("Sing-box autostart disabled"), value: "" }, { state: data.sing_box_process_running ? "success" : "error", key: _("Sing-box process running"), value: "" }, { state: data.sing_box_ports_listening ? "success" : "error", key: _("Sing-box listening ports"), value: "" } ] }); if (!atLeastOneGood || !data.sing_box_process_running) { throw new Error("Sing-box checks failed"); } } // src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts function serverPrefix(item) { return `${item.label}:`; } function formatListen(item) { return `${item.listen}:${Number(item.listen_port || 0)} [${item.required_proto}]`; } function getPortConflictItem(item) { const key = `${serverPrefix(item)} ${_("Port conflict")}`; if (item.port_conflict === 1) { return { state: "error", key, value: `${_("Used by")}: ${item.port_conflict_owners || _("unknown")}` }; } return { state: "success", key, value: _("No conflict detected") }; } function getPublicHostItem(item, wanIp) { const key = `${serverPrefix(item)} ${_("Public host")}`; if (!item.public_host) { return { state: "warning", key, value: _("Not configured") }; } if (item.public_host_resolved === 0) { return { state: "warning", key, value: `${item.public_host} (${_("Does not resolve")})` }; } if (item.public_host_public === 0) { return { state: "warning", key, value: `${item.public_host} (${_("Not public")})` }; } if (item.public_host_matches_wan === 0) { return { state: "warning", key, value: `${item.public_host_ips || item.public_host} / ${_("WAN")}: ${wanIp || _("Not detected")}` }; } return { state: "success", key, value: item.public_host }; } function getServerItems(item, wanIp) { const prefix = serverPrefix(item); const items = [ { state: item.runtime_ok ? "success" : "error", key: `${prefix} ${_("Generated inbound")}`, value: `${item.tag} [${item.protocol}]` } ]; if (item.protocol === "tailscale") { items.push( { state: "success", key: `${prefix} ${_("Tailscale endpoint")}`, value: _("No public firewall port required") }, { state: item.routes_configured ? "success" : "warning", key: `${prefix} ${_("Routing rules")}`, value: item.routing_mode } ); return items; } if (item.protocol === "json_inbound") { items.push({ state: item.routes_configured ? "success" : "warning", key: `${prefix} ${_("Routing rules")}`, value: item.routing_mode }); return items; } items.push( { state: item.listening === 1 && item.port_conflict !== 1 ? "success" : "error", key: `${prefix} ${_("Listening port")}`, value: formatListen(item) }, getPortConflictItem(item), { state: item.firewall_required === 0 ? "warning" : item.firewall_open === 1 ? "success" : "error", key: `${prefix} ${_("Firewall WAN port")}`, value: item.firewall_required === 0 ? _("Not required for this listen address") : `${item.required_proto}/${Number(item.listen_port || 0)}` }, { state: item.routes_configured ? "success" : "warning", key: `${prefix} ${_("Routing rules")}`, value: item.routing_mode }, getPublicHostItem(item, wanIp) ); return items; } async function runInboundsCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.INBOUNDS; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const inboundsChecks = await TachyonShellMethods.checkInbounds(); if (!inboundsChecks.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Inbounds checks failed"); } const data = inboundsChecks.data; if (!data.enabled_count) { updateCheckStore({ order, code, title, description: _("No enabled server inbounds configured"), state: "skipped", items: [] }); return; } const items = [ { state: data.wan_public ? "success" : "warning", key: _("WAN public IP"), value: data.wan_ip || _("Not detected") } ]; data.items.forEach((item) => { items.push(...getServerItems(item, data.wan_ip)); }); const allGood = items.every((item) => item.state === "success"); const atLeastOneGood = items.some((item) => item.state !== "error"); const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, code, title, description, state, items }); if (!atLeastOneGood) { throw new Error("Inbounds checks failed"); } } // src/tachyon/tabs/diagnostic/checks/runNftCheck.ts async function runNftCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); await RemoteFakeIPMethods.getFakeIpCheck(); await RemoteFakeIPMethods.getIpCheck(); const nftablesChecks = await TachyonShellMethods.checkNftRules(); if (!nftablesChecks.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Nftables checks failed"); } const data = nftablesChecks.data; const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_mangle_output_counters) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_mangle_output_counters) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, code, title, description, state, items: [ { state: data.table_exist ? "success" : "error", key: _("Table exist"), value: "" }, { state: data.rules_mangle_exist ? "success" : "error", key: _("Rules mangle exist"), value: "" }, { state: data.rules_mangle_counters ? "success" : "warning", key: _("Rules mangle counters"), value: "" }, { state: data.rules_mangle_output_exist ? "success" : "error", key: _("Rules mangle output exist"), value: "" }, { state: data.rules_mangle_output_counters ? "success" : "error", key: _("Rules mangle output counters"), value: "" }, { state: data.rules_proxy_exist ? "success" : "error", key: _("Rules proxy exist"), value: "" }, { state: data.rules_proxy_counters ? "success" : "error", key: _("Rules proxy counters"), value: "" }, { state: !data.rules_other_mark_exist ? "success" : "warning", key: !data.rules_other_mark_exist ? _("No other marking rules found") : _("Additional marking rules found"), value: "" } ] }); if (!atLeastOneGood) { throw new Error("Nftables checks failed"); } } // src/tachyon/tabs/diagnostic/checks/runFakeIPCheck.ts async function runFakeIPCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const routerFakeIPResponse = await TachyonShellMethods.checkFakeIP(); const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); const browserFakeIPCheckUnavailable = !checkFakeIPResponse.success; const browserFakeIPCheckMessage = checkFakeIPResponse.success ? "" : checkFakeIPResponse.message; const checks = { singBoxFakeIP: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, browserFakeIP: checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, canComparePublicIP: checkFakeIPResponse.success && checkIPResponse.success, differentIP: checkFakeIPResponse.success && checkIPResponse.success && checkFakeIPResponse.data.IP !== checkIPResponse.data.IP }; const fakeIPWorks = checks.singBoxFakeIP && checks.browserFakeIP; const { state, description } = fakeIPWorks ? checks.differentIP ? { state: "success", description: _("Checks passed") } : { state: "warning", description: _("FakeIP works; public IP comparison is inconclusive") } : browserFakeIPCheckUnavailable && checks.singBoxFakeIP ? { state: "warning", description: _("Browser FakeIP check could not be completed") } : getMeta({ allGood: false, atLeastOneGood: checks.singBoxFakeIP || checks.browserFakeIP }); updateCheckStore({ order, code, title, description, state, items: [ { state: checks.singBoxFakeIP ? "success" : "error", key: checks.singBoxFakeIP ? _("Sing-box FakeIP DNS works") : _("Sing-box FakeIP DNS does not work"), value: routerFakeIPResponse.success ? routerFakeIPResponse.data.IP : "" }, { state: browserFakeIPCheckUnavailable ? "warning" : checks.browserFakeIP ? "success" : "error", key: browserFakeIPCheckUnavailable ? _("Browser FakeIP check could not be completed") : checks.browserFakeIP ? _("Browser is using FakeIP correctly") : _("Browser is not using FakeIP"), value: browserFakeIPCheckMessage }, ...insertIf(checks.browserFakeIP, [ { state: checks.differentIP ? "success" : "warning", key: !checks.canComparePublicIP ? _("Could not compare FakeIP and control public IPs") : checks.differentIP ? _("FakeIP and control checks use different public IPs") : _("FakeIP and control checks use the same public IP"), value: "" } ]) ] }); } // src/tachyon/tabs/diagnostic/checks/getCheckItemsMeta.ts function getCheckItemsMeta(items) { if (items.some((item) => item.state === "error")) { return { state: "error", description: _("Checks failed") }; } if (items.some((item) => item.state === "warning")) { return { state: "warning", description: _("Issues detected") }; } return { state: "success", description: _("Checks passed") }; } // src/tachyon/tabs/diagnostic/checks/runZapretCheck.ts async function runZapretCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.ZAPRET; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const zapretStatus = await TachyonShellMethods.getZapretStatus(); if (!zapretStatus.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Zapret checks failed"); } const data = zapretStatus.data; const providerAvailable = Boolean(data.provider_available ?? data.installed); const packageInstalled = Boolean(data.package_installed); const hasZapretRules = Number(data.enabled_rule_count || 0) > 0; const queueOverlap = Boolean(data.queue_overlap); const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneConflict = hasZapretRules && standaloneServiceRunning; const expectedProcesses = Number(data.expected_process_count || 0); const runningProcesses = Number(data.running_process_count || 0); const supervisorProcesses = Number(data.supervisor_process_count || 0); const tachyonRuntimeReady = !hasZapretRules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasZapretRules && (runningProcesses > 0 || supervisorProcesses > 0); const outboundsConfigured = Boolean(data.outbounds_configured); const items = [ { state: providerAvailable ? "success" : hasZapretRules ? "error" : "warning", key: providerAvailable ? _("Zapret provider binary is available") : _("Zapret provider binary is not available"), value: data.provider_path || "" }, { state: packageInstalled ? "success" : hasZapretRules ? "error" : "warning", key: packageInstalled ? _("Zapret package is installed") : _("Zapret package is not installed"), value: "" }, { state: hasZapretRules && !providerAvailable ? "error" : "success", key: hasZapretRules ? _("There are rules using Zapret") : _("No rules use Zapret"), value: "" }, { state: unexpectedRuntime || !tachyonRuntimeReady ? "error" : "success", key: hasZapretRules ? tachyonRuntimeReady ? _("Tachyon-managed nfqws runtime is ready") : _("Tachyon-managed nfqws runtime is not ready") : unexpectedRuntime ? _("Unexpected Tachyon-managed nfqws runtime is running") : _("Tachyon-managed nfqws runtime is not running"), value: hasZapretRules ? `${runningProcesses}/${expectedProcesses}` : "" }, { state: queueOverlap ? "error" : "success", key: queueOverlap ? _("NFQUEUE range overlaps with another rule") : _("NFQUEUE range is available"), value: `${Number(data.queue_base || 0)}-${Number(data.queue_range_end || 0)}` }, { state: !hasZapretRules || outboundsConfigured ? "success" : "error", key: outboundsConfigured ? _("Zapret sing-box outbound is configured") : _("Zapret sing-box outbound is not configured"), value: "" }, { state: standaloneConflict ? "warning" : "success", key: standaloneServiceRunning ? hasZapretRules ? _("Standalone Zapret is active together with Tachyon Zapret rules") : _("Standalone Zapret service is active") : _("Standalone Zapret service is inactive"), value: "" } ]; const { state, description } = getCheckItemsMeta(items); updateCheckStore({ order, code, title, description, state, items }); } // src/tachyon/tabs/diagnostic/checks/runZapret2Check.ts async function runZapret2Check() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.ZAPRET2; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const zapret2Status = await TachyonShellMethods.getZapret2Status(); if (!zapret2Status.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Zapret2 checks failed"); } const data = zapret2Status.data; const providerAvailable = Boolean(data.provider_available ?? data.installed); const packageInstalled = Boolean(data.package_installed); const hasZapret2Rules = Number(data.enabled_rule_count || 0) > 0; const queueOverlap = Boolean(data.queue_overlap); const expectedProcesses = Number(data.expected_process_count || 0); const runningProcesses = Number(data.running_process_count || 0); const supervisorProcesses = Number(data.supervisor_process_count || 0); const tachyonRuntimeReady = !hasZapret2Rules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasZapret2Rules && (runningProcesses > 0 || supervisorProcesses > 0); const outboundsConfigured = Boolean(data.outbounds_configured); const standaloneServiceEnabled = Boolean(data.standalone_service_enabled); const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneConflict = hasZapret2Rules && standaloneServiceRunning; const standaloneAutostartRisk = hasZapret2Rules && standaloneServiceEnabled && !standaloneServiceRunning; const items = [ { state: providerAvailable ? "success" : hasZapret2Rules ? "error" : "warning", key: providerAvailable ? _("Zapret2 provider binary is available") : _("Zapret2 provider binary is not available"), value: data.provider_path || "" }, { state: packageInstalled ? "success" : hasZapret2Rules ? "error" : "warning", key: packageInstalled ? _("Zapret2 package is installed") : _("Zapret2 package is not installed"), value: "" }, { state: hasZapret2Rules && !providerAvailable ? "error" : "success", key: hasZapret2Rules ? _("There are rules using Zapret2") : _("No rules use Zapret2"), value: "" }, { state: unexpectedRuntime || !tachyonRuntimeReady ? "error" : "success", key: hasZapret2Rules ? tachyonRuntimeReady ? _("Tachyon-managed nfqws2 runtime is ready") : _("Tachyon-managed nfqws2 runtime is not ready") : unexpectedRuntime ? _("Unexpected Tachyon-managed nfqws2 runtime is running") : _("Tachyon-managed nfqws2 runtime is not running"), value: hasZapret2Rules ? `${runningProcesses}/${expectedProcesses}` : "" }, { state: queueOverlap ? "error" : "success", key: queueOverlap ? _("NFQUEUE range overlaps with another rule") : _("NFQUEUE range is available"), value: `${Number(data.queue_base || 0)}-${Number(data.queue_range_end || 0)}` }, { state: !hasZapret2Rules || outboundsConfigured ? "success" : "error", key: outboundsConfigured ? _("Zapret2 sing-box outbound is configured") : _("Zapret2 sing-box outbound is not configured"), value: "" }, { state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success", key: standaloneServiceRunning ? hasZapret2Rules ? _( "Standalone Zapret2 is active together with Tachyon Zapret2 rules" ) : _("Standalone Zapret2 service is active") : standaloneAutostartRisk ? _("Standalone Zapret2 autostart is enabled") : _("Standalone Zapret2 service is inactive"), value: "" } ]; const { state, description } = getCheckItemsMeta(items); updateCheckStore({ order, code, title, description, state, items }); } // src/tachyon/tabs/diagnostic/checks/runByedpiCheck.ts async function runByedpiCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.BYEDPI; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const byedpiStatus = await TachyonShellMethods.getByedpiStatus(); if (!byedpiStatus.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("ByeDPI checks failed"); } const data = byedpiStatus.data; const providerAvailable = Boolean(data.provider_available ?? data.installed); const packageInstalled = Boolean(data.package_installed); const hasByedpiRules = Number(data.enabled_rule_count || 0) > 0; const expectedProcesses = Number(data.expected_process_count || 0); const runningProcesses = Number(data.running_process_count || 0); const supervisorProcesses = Number(data.supervisor_process_count || 0); const restartCount = Number(data.restart_count || 0); const runtimeUnstable = Boolean(data.runtime_unstable); const tachyonRuntimeReady = !hasByedpiRules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasByedpiRules && (runningProcesses > 0 || supervisorProcesses > 0); const outboundsConfigured = Boolean(data.outbounds_configured); const standaloneServiceEnabled = Boolean(data.standalone_service_enabled); const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneConflict = hasByedpiRules && standaloneServiceRunning; const standaloneAutostartRisk = hasByedpiRules && standaloneServiceEnabled && !standaloneServiceRunning; const items = [ { state: providerAvailable ? "success" : hasByedpiRules ? "error" : "warning", key: providerAvailable ? _("ByeDPI provider binary is available") : _("ByeDPI provider binary is not available"), value: data.provider_path || "" }, { state: packageInstalled ? "success" : "warning", key: packageInstalled ? _("ByeDPI package is installed") : _("ByeDPI package is not installed"), value: "" }, { state: hasByedpiRules && !providerAvailable ? "error" : "success", key: hasByedpiRules ? _("There are rules using ByeDPI") : _("No rules use ByeDPI"), value: "" }, { state: unexpectedRuntime || !tachyonRuntimeReady ? "error" : runtimeUnstable ? "warning" : "success", key: hasByedpiRules ? runtimeUnstable ? _("Tachyon-managed ciadpi runtime has restarted") : tachyonRuntimeReady ? _("Tachyon-managed ciadpi runtime is ready") : _("Tachyon-managed ciadpi runtime is not ready") : unexpectedRuntime ? _("Unexpected Tachyon-managed ciadpi runtime is running") : _("Tachyon-managed ciadpi runtime is not running"), value: hasByedpiRules ? runtimeUnstable ? `${restartCount}` : `${runningProcesses}/${expectedProcesses}` : "" }, { state: !hasByedpiRules || outboundsConfigured ? "success" : "error", key: outboundsConfigured ? _("ByeDPI sing-box outbound is configured") : _("ByeDPI sing-box outbound is not configured"), value: `${data.listen_address}:${Number(data.port_base || 0)}` }, { state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success", key: standaloneServiceRunning ? hasByedpiRules ? _("Standalone ByeDPI is active together with Tachyon ByeDPI rules") : _("Standalone ByeDPI service is active") : standaloneAutostartRisk ? _("Standalone ByeDPI autostart is enabled") : _("Standalone ByeDPI service is inactive"), value: "" } ]; const { state, description } = getCheckItemsMeta(items); updateCheckStore({ order, code, title, description, state, items }); } // src/tachyon/services/systemInfo.service.ts var UNKNOWN_SYSTEM_INFO = { loading: false, loaded: false, providerInfoLoaded: false, tachyon_version: _("unknown"), tachyon_commit_sha: "", tachyon_latest_version: _("unknown"), luci_app_version: _("unknown"), sing_box_version: _("unknown"), sing_box_extended: 0, sing_box_tiny: 0, sing_box_compressed: 0, sing_box_lx: 0, sing_box_tailscale: 1, sing_box_repo_url: "", zapret_version: _("unknown"), zapret_installed: 0, zapret2_version: _("unknown"), zapret2_installed: 0, byedpi_version: _("unknown"), byedpi_installed: 0, server_inbounds_enabled_count: -1, openwrt_version: _("unknown"), device_model: _("unknown") }; var systemInfoPromise = null; var latestSystemInfoRequestId = 0; function hasLoadedSystemInfo() { const systemInfo = store.get().diagnosticsSystemInfo; return Boolean(systemInfo.loaded) && !systemInfo.loading; } async function ensureSystemInfo({ force = false, silent = false } = {}) { if (!force && hasLoadedSystemInfo()) { return store.get().diagnosticsSystemInfo; } if (systemInfoPromise && !force) { return systemInfoPromise; } const requestId = ++latestSystemInfoRequestId; const currentSystemInfo = store.get().diagnosticsSystemInfo; if (!silent) { store.set({ diagnosticsSystemInfo: { ...currentSystemInfo, loading: true } }); } const promise = (async () => { try { const systemInfo = await TachyonShellMethods.getSystemInfo(); if (requestId !== latestSystemInfoRequestId) { return store.get().diagnosticsSystemInfo; } if (systemInfo.success) { const nextSystemInfo = normalizeSingBoxVariantFields({ ...UNKNOWN_SYSTEM_INFO, loading: false, loaded: true, providerInfoLoaded: true, server_inbounds_enabled_count: currentSystemInfo.server_inbounds_enabled_count, ...systemInfo.data }); store.set({ diagnosticsSystemInfo: nextSystemInfo }); return nextSystemInfo; } } catch (error) { logger.error("[SYSTEM_INFO]", "ensureSystemInfo failed", error); } if (requestId === latestSystemInfoRequestId && !silent) { const latestSystemInfo = store.get().diagnosticsSystemInfo; const nextSystemInfo = { ...UNKNOWN_SYSTEM_INFO, loading: false, loaded: false, providerInfoLoaded: latestSystemInfo.providerInfoLoaded, zapret_installed: latestSystemInfo.zapret_installed, zapret2_installed: latestSystemInfo.zapret2_installed, byedpi_installed: latestSystemInfo.byedpi_installed, server_inbounds_enabled_count: latestSystemInfo.server_inbounds_enabled_count }; store.set({ diagnosticsSystemInfo: nextSystemInfo }); return nextSystemInfo; } return store.get().diagnosticsSystemInfo; })(); systemInfoPromise = promise; try { return await promise; } finally { if (systemInfoPromise === promise) { systemInfoPromise = null; } } } // src/partials/button/styles.ts var styles2 = ` .tachyon-partial-button { text-align: center; } .tachyon-partial-button--with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 5px; } .tachyon-partial-button--loading { } .tachyon-partial-button--disabled { } .tachyon-partial-button__icon { flex: 0 0 auto; } .tachyon-partial-button__icon { display: inline-flex; align-items: center; justify-content: center; } .tachyon-partial-button__icon svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } `; // src/partials/modal/styles.ts var styles3 = ` .tachyon-partial-modal__body {} .tachyon-partial-modal__content { max-height: 70vh; overflow: scroll; border-radius: 4px; } .tachyon-partial-modal__footer { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 10px; } .tachyon-partial-modal__footer button { margin-left: 0; } .tachyon-partial-modal__checkbox { display: inline-flex; align-items: center; gap: 8px; margin-right: auto; cursor: pointer; user-select: none; } .tachyon-partial-modal__checkbox-text { line-height: 1.2; } `; // src/partials/button/renderButton.ts function renderButton({ classNames = [], disabled, loading: loading2, onClick, text, icon }) { const hasIcon = !!loading2 || !!icon; function getWrappedIcon() { const iconWrap = E("span", { class: "tachyon-partial-button__icon" }); if (loading2) { iconWrap.appendChild(renderLoaderCircleIcon24()); return iconWrap; } if (icon) { iconWrap.appendChild(icon()); return iconWrap; } return iconWrap; } function getClass() { return [ "btn", "tachyon-partial-button", ...insertIf(Boolean(disabled), ["tachyon-partial-button--disabled"]), ...insertIf(Boolean(loading2), ["tachyon-partial-button--loading"]), ...insertIf(Boolean(hasIcon), ["tachyon-partial-button--with-icon"]), ...classNames ].filter(Boolean).join(" "); } function getDisabled() { if (loading2 || disabled) { return true; } return void 0; } return E( "button", { type: "button", class: getClass(), disabled: getDisabled(), click: onClick }, [...insertIf(hasIcon, [getWrappedIcon()]), E("span", {}, text)] ); } // src/partials/modal/renderModal.ts function renderModal(text, name, options) { let rawText = text ?? ""; let currentText = ""; let refreshInFlight = false; let pendingRefresh = false; let pendingForcedRefresh = false; let refreshSessionId = 0; let timer; let observer; let autoRefreshEnabled = options?.initialAutoRefresh ?? Boolean(options?.getText); let maskValuesEnabled = options?.initialMaskValues ?? true; let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd); let autoRefreshInput; let maskValuesInput; const getDisplayText = (value) => { if (maskValuesEnabled && options?.maskText) { return options.maskText(value); } return value; }; const codeEl = E("code", {}, ""); const contentEl = E( "pre", { class: "tachyon-partial-modal__content" }, codeEl ); const stopRefreshTimer = () => { if (timer) { clearInterval(timer); timer = void 0; } }; const destroyLiveRefresh = () => { refreshSessionId += 1; pendingRefresh = false; pendingForcedRefresh = false; stopRefreshTimer(); observer?.disconnect(); observer = void 0; }; const scrollToBottom = () => { contentEl.scrollTop = contentEl.scrollHeight; }; const scheduleInitialScrollToBottom = () => { if (!shouldScrollToBottomOnMount || !body.isConnected) { return; } shouldScrollToBottomOnMount = false; requestAnimationFrame(() => { scrollToBottom(); }); }; const updateText = (nextText) => { const normalizedText = nextText ?? ""; const shouldStickToBottom = shouldScrollToBottomOnMount || contentEl.scrollTop + contentEl.clientHeight >= contentEl.scrollHeight - 16; if (normalizedText === currentText) { if (shouldStickToBottom) { requestAnimationFrame(() => { scrollToBottom(); }); } return; } currentText = normalizedText; codeEl.textContent = currentText; if (shouldStickToBottom) { requestAnimationFrame(() => { scrollToBottom(); }); } }; const updateDisplayedTextFromRaw = () => { updateText(getDisplayText(rawText)); }; const refreshText = async (force = false) => { if (!options?.getText || !force && !autoRefreshEnabled || refreshInFlight) { return; } if (!body.isConnected) { return; } refreshInFlight = true; const sessionId = refreshSessionId; try { const nextText = await options.getText({ maskValues: options.maskText ? false : maskValuesEnabled }); if (!body.isConnected || !force && !autoRefreshEnabled) { return; } if (sessionId !== refreshSessionId) { return; } const normalizedText = nextText ?? ""; rawText = normalizedText; updateText(getDisplayText(normalizedText)); } catch (error) { console.warn("[renderModal] failed to refresh modal content", error); } finally { refreshInFlight = false; if (pendingRefresh) { const shouldForceRefresh = pendingForcedRefresh; pendingRefresh = false; pendingForcedRefresh = false; if ((shouldForceRefresh || autoRefreshEnabled) && body.isConnected) { void refreshText(shouldForceRefresh); } } } }; const requestRefresh = () => { if (!options?.getText || !autoRefreshEnabled) { return; } if (refreshInFlight) { pendingRefresh = true; return; } void refreshText(); }; const requestForcedRefresh = () => { if (!options?.getText) { return; } if (refreshInFlight) { pendingRefresh = true; pendingForcedRefresh = true; return; } void refreshText(true); }; const startRefreshTimer = () => { if (!options?.getText || !autoRefreshEnabled || timer || typeof document === "undefined") { return; } timer = setInterval(() => { requestRefresh(); }, options.refreshMs ?? 3e3); }; const setAutoRefreshEnabled = (nextValue) => { autoRefreshEnabled = nextValue; refreshSessionId += 1; pendingRefresh = false; pendingForcedRefresh = false; if (autoRefreshInput) { autoRefreshInput.checked = nextValue; } if (nextValue) { startRefreshTimer(); requestRefresh(); return; } stopRefreshTimer(); }; const setMaskValuesEnabled = (nextValue) => { maskValuesEnabled = nextValue; refreshSessionId += 1; pendingRefresh = false; pendingForcedRefresh = false; if (maskValuesInput) { maskValuesInput.checked = nextValue; } if (options?.maskText) { updateDisplayedTextFromRaw(); return; } requestForcedRefresh(); }; const footerChildren = [ renderButton({ classNames: ["cbi-button-apply"], text: _("Download"), onClick: () => downloadAsTxt(currentText, name) }), renderButton({ classNames: ["cbi-button-apply"], text: _("Copy"), onClick: () => copyToClipboard(`\`\`\`${name} ${currentText} \`\`\``) }), renderButton({ classNames: ["cbi-button-remove"], text: _("Close"), onClick: () => { destroyLiveRefresh(); ui.hideModal(); } }) ]; if (options?.getText && options?.showAutoRefreshToggle) { autoRefreshInput = document.createElement("input"); autoRefreshInput.type = "checkbox"; autoRefreshInput.className = "cbi-input-checkbox"; autoRefreshInput.checked = autoRefreshEnabled; autoRefreshInput.addEventListener("change", () => { setAutoRefreshEnabled(autoRefreshInput.checked); }); footerChildren.unshift( E("label", { class: "tachyon-partial-modal__checkbox" }, [ autoRefreshInput, E( "span", { class: "tachyon-partial-modal__checkbox-text" }, options.autoRefreshLabel ?? _("Auto refresh") ) ]) ); } if ((options?.getText || options?.maskText) && options?.showMaskValuesToggle) { maskValuesInput = document.createElement("input"); maskValuesInput.type = "checkbox"; maskValuesInput.className = "cbi-input-checkbox"; maskValuesInput.checked = maskValuesEnabled; maskValuesInput.addEventListener("change", () => { setMaskValuesEnabled(maskValuesInput.checked); }); footerChildren.unshift( E("label", { class: "tachyon-partial-modal__checkbox" }, [ maskValuesInput, E( "span", { class: "tachyon-partial-modal__checkbox-text" }, options.maskValuesLabel ?? _("Hide values") ) ]) ); } const body = E("div", { class: "tachyon-partial-modal__body" }, [ E("div", {}, [ contentEl, E("div", { class: "tachyon-partial-modal__footer" }, footerChildren) ]) ]); if ((options?.getText || options?.startAtEnd) && typeof document !== "undefined") { observer = new MutationObserver(() => { if (!body.isConnected) { destroyLiveRefresh(); return; } scheduleInitialScrollToBottom(); }); observer.observe(document.body, { childList: true, subtree: true }); scheduleInitialScrollToBottom(); } if (options?.getText && typeof document !== "undefined") { startRefreshTimer(); requestRefresh(); } updateDisplayedTextFromRaw(); return body; } // src/partials/index.ts var PartialStyles = ` ${styles2} ${styles3} `; // src/tachyon/tabs/diagnostic/partials/renderAvailableActions.ts function renderAvailableActions({ restart, start, stop, enable, disable, globalCheck, doctor, aiDoctor, aiChat, viewLogs, showSingBoxConfig, generateBugReport, checkServices }) { return E("div", { class: "tachyon_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), ...insertIf(restart.visible, [ renderButton({ classNames: ["cbi-button-apply"], onClick: restart.onClick, icon: renderRotateCcwIcon24, text: _("Restart Tachyon"), loading: restart.loading, disabled: restart.disabled }) ]), ...insertIf(stop.visible, [ renderButton({ classNames: ["cbi-button-remove"], onClick: stop.onClick, icon: renderCircleStopIcon24, text: _("Stop Tachyon"), loading: stop.loading, disabled: stop.disabled }) ]), ...insertIf(start.visible, [ renderButton({ classNames: ["cbi-button-save"], onClick: start.onClick, icon: renderCirclePlayIcon24, text: _("Start Tachyon"), loading: start.loading, disabled: start.disabled }) ]), ...insertIf(disable.visible, [ renderButton({ classNames: ["cbi-button-remove"], onClick: disable.onClick, icon: renderPauseIcon24, text: _("Disable autostart"), loading: disable.loading, disabled: disable.disabled }) ]), ...insertIf(enable.visible, [ renderButton({ classNames: ["cbi-button-save"], onClick: enable.onClick, icon: renderPlayIcon24, text: _("Enable autostart"), loading: enable.loading, disabled: enable.disabled }) ]), ...insertIf(globalCheck.visible, [ renderButton({ onClick: globalCheck.onClick, icon: renderCircleCheckBigIcon24, text: _("Get global check"), loading: globalCheck.loading, disabled: globalCheck.disabled }) ]), ...insertIf(doctor.visible, [ renderButton({ onClick: doctor.onClick, icon: renderRotateCcwIcon24, text: _("Run doctor repair"), loading: doctor.loading, disabled: doctor.disabled }) ]), ...insertIf(aiDoctor.visible, [ renderButton({ classNames: ["cbi-button-action"], onClick: aiDoctor.onClick, icon: renderRotateCcwIcon24, text: _("Run AI Doctor"), loading: aiDoctor.loading, disabled: aiDoctor.disabled }) ]), ...insertIf(!!aiChat?.visible, [ renderButton({ classNames: ["cbi-button-action"], onClick: aiChat.onClick, icon: renderCircleCheckBigIcon24, text: _("AI Chat Assistant"), loading: aiChat.loading, disabled: aiChat.disabled }) ]), ...insertIf(checkServices.visible, [ renderButton({ classNames: ["cbi-button-action"], onClick: checkServices.onClick, icon: renderSquareChartGanttIcon24, text: _("Check Services"), loading: checkServices.loading, disabled: checkServices.disabled }) ]), ...insertIf(viewLogs.visible, [ renderButton({ onClick: viewLogs.onClick, icon: renderSquareChartGanttIcon24, text: _("View logs"), loading: viewLogs.loading, disabled: viewLogs.disabled }) ]), ...insertIf(showSingBoxConfig.visible, [ renderButton({ onClick: showSingBoxConfig.onClick, icon: renderCogIcon24, text: _("Show sing-box config"), loading: showSingBoxConfig.loading, disabled: showSingBoxConfig.disabled }) ]), ...insertIf(generateBugReport.visible, [ renderButton({ onClick: generateBugReport.onClick, icon: renderDownloadIcon24, text: _("Generate bug report"), loading: generateBugReport.loading, disabled: generateBugReport.disabled }) ]) ]); } // src/tachyon/tabs/diagnostic/partials/renderCheckSection.ts function renderCheckSummary(items) { if (!items.length) { return E("div", {}, ""); } const renderedItems = items.map((item) => { function getIcon() { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__summary__item__icon" }); if (item.state === "success") { iconWrap.appendChild(renderCheckIcon24()); } if (item.state === "warning") { iconWrap.appendChild(renderTriangleAlertIcon24()); } if (item.state === "error") { iconWrap.appendChild(renderXIcon24()); } return iconWrap; } return E( "div", { class: `tachyon_diagnostic_alert__summary__item tachyon_diagnostic_alert__summary__item--${item.state}` }, [getIcon(), E("b", {}, item.key), E("div", {}, item.value)] ); }); return E( "div", { class: "tachyon_diagnostic_alert__summary" }, renderedItems ); } function renderLoadingState3(props) { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__icon" }); iconWrap.appendChild(renderLoaderCircleIcon24()); return E( "div", { class: "tachyon_diagnostic_alert tachyon_diagnostic_alert--loading" }, [ iconWrap, E("div", { class: "tachyon_diagnostic_alert__content" }, [ E("b", { class: "tachyon_diagnostic_alert__title" }, props.title), E( "div", { class: "tachyon_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderWarningState(props) { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleAlertIcon24()); return E( "div", { class: "tachyon_diagnostic_alert tachyon_diagnostic_alert--warning" }, [ iconWrap, E("div", { class: "tachyon_diagnostic_alert__content" }, [ E("b", { class: "tachyon_diagnostic_alert__title" }, props.title), E( "div", { class: "tachyon_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderErrorState(props) { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleXIcon24()); return E( "div", { class: "tachyon_diagnostic_alert tachyon_diagnostic_alert--error" }, [ iconWrap, E("div", { class: "tachyon_diagnostic_alert__content" }, [ E("b", { class: "tachyon_diagnostic_alert__title" }, props.title), E( "div", { class: "tachyon_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSuccessState(props) { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleCheckIcon24()); return E( "div", { class: "tachyon_diagnostic_alert tachyon_diagnostic_alert--success" }, [ iconWrap, E("div", { class: "tachyon_diagnostic_alert__content" }, [ E("b", { class: "tachyon_diagnostic_alert__title" }, props.title), E( "div", { class: "tachyon_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSkippedState(props) { const iconWrap = E("span", { class: "tachyon_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleSlashIcon24()); return E( "div", { class: "tachyon_diagnostic_alert tachyon_diagnostic_alert--skipped" }, [ iconWrap, E("div", { class: "tachyon_diagnostic_alert__content" }, [ E("b", { class: "tachyon_diagnostic_alert__title" }, props.title), E( "div", { class: "tachyon_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderCheckSection(props) { if (props.state === "loading") { return renderLoadingState3(props); } if (props.state === "warning") { return renderWarningState(props); } if (props.state === "error") { return renderErrorState(props); } if (props.state === "success") { return renderSuccessState(props); } if (props.state === "skipped") { return renderSkippedState(props); } return E("div", {}, _("Not implement yet")); } // src/tachyon/tabs/diagnostic/partials/renderRunAction.ts function renderRunAction({ loading: loading2, disabled, click }) { return E("div", { class: "tachyon_diagnostic-page__run_check_wrapper" }, [ renderButton({ text: _("Run Diagnostic"), onClick: click, icon: renderSearchIcon24, loading: loading2, disabled, classNames: ["cbi-button-apply"] }) ]); } // src/tachyon/tabs/diagnostic/partials/renderServiceCheckModal.ts function formatRouteType(routeType) { if (!routeType) return "\u041F\u0440\u044F\u043C\u043E\u0435"; const clean = routeType.trim(); if (clean === "connection" || clean === "proxy" || clean === "outbound") { return "\u041F\u0440\u043E\u043A\u0441\u0438"; } if (clean === "direct") { return "\u041F\u0440\u044F\u043C\u043E\u0435"; } if (clean === "auto") { return "\u0410\u0432\u0442\u043E"; } if (clean.startsWith("zapret2")) { return clean.replace(/^zapret2/, "Zapret 2"); } if (clean.startsWith("zapret")) { return clean.replace(/^zapret/, "Zapret"); } if (clean.startsWith("byedpi")) { return clean.replace(/^byedpi/, "ByeDPI"); } return clean; } function formatSectionName(name) { if (!name) return ""; const clean = name.trim(); const sectionMap = { "\u0411\u0430\u0437\u043E\u0432\u0430\u044F \u0441\u0432\u044F\u0437\u043D\u043E\u0441\u0442\u044C": "\u0411\u0430\u0437\u043E\u0432\u0430\u044F \u0441\u0432\u044F\u0437\u043D\u043E\u0441\u0442\u044C", "\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0432 \u0420\u0424": "\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044B", "\u0421\u0432\u043E\u0438 \u0434\u043E\u043C\u0435\u043D\u044B": "\u0421\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0435 \u0434\u043E\u043C\u0435\u043D\u044B", Custom: "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439", "ChatGPT / OpenAI": "ChatGPT / OpenAI", "Gemini / Google AI": "Gemini / Google AI", "X (Twitter)": "Twitter (X)", "UDP / QUIC": "UDP / QUIC \u043F\u0440\u043E\u0442\u043E\u043A\u043E\u043B\u044B", Telegram: "Telegram", Instagram: "Instagram", YouTube: "YouTube", TikTok: "TikTok", Discord: "Discord", Facebook: "Facebook", WhatsApp: "WhatsApp", Netflix: "Netflix", Spotify: "Spotify", Twitch: "Twitch", Steam: "Steam", GitHub: "GitHub" }; return sectionMap[clean] || clean; } function renderStatusBadge(statusClass, isTesting = false, isPending = false) { if (isPending) { return E( "span", { class: "badge cbi-value-title", style: "opacity: 0.6; font-size: 11px; padding: 2px 8px;" }, "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435..." ); } if (isTesting) { return E( "span", { class: "badge cbi-button-action", style: "font-size: 11px; padding: 2px 8px;" }, "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430..." ); } const cleanStatus = statusClass || ""; const isOk = cleanStatus.toUpperCase().includes("OK") || cleanStatus === "200" || cleanStatus === "OK"; if (isOk) { return E( "span", { class: "badge cbi-button-save", style: "font-weight: bold; font-size: 11px; padding: 2px 8px;" }, "\u0414\u043E\u0441\u0442\u0443\u043F\u0435\u043D" ); } return E( "span", { class: "badge cbi-button-reset", style: "font-weight: bold; font-size: 11px; padding: 2px 8px;" }, cleanStatus || "\u041D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" ); } function renderServiceCheckModal() { const container = E( "div", { class: "tachyon-service-check-modal-wrapper", style: "width: 100%; box-sizing: border-box;" }, [ E( "p", { style: "text-align: center; margin-top: 20px;" }, "\u041F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u0435 \u0441\u043F\u0438\u0441\u043A\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432..." ), E( "div", { class: "spinning", style: "text-align: center; font-size: 24px; color: var(--border-color, #007bff);" }, "\u26A1" ) ] ); const modalContent = E( "div", { style: "width: 100%; box-sizing: border-box;" }, [ container, E( "div", { id: "tachyon-service-check-footer", style: "margin-top: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px; border-top: 1px solid var(--border-color, rgba(128,128,128,0.2)); padding-top: 12px; flex-wrap: wrap;" }, [ renderButton({ text: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C", onClick: () => ui.hideModal() }) ] ) ] ); ui.showModal("\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432", modalContent); const loadTargets = (targetMode) => { container.innerHTML = ""; container.appendChild( E( "p", { style: "text-align: center; margin-top: 20px;" }, "\u041F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u0435 \u0441\u043F\u0438\u0441\u043A\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432..." ) ); const args = targetMode === "all" ? ["get-targets", "all"] : ["get-targets"]; callBaseMethod(Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK, args).then((res) => { const response = res; container.innerHTML = ""; if (!response || !response.success) { container.appendChild( E( "p", { style: "color: var(--color-danger, #dc3545);" }, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432." ) ); return; } let targets = []; try { if (typeof response.data === "string") { targets = JSON.parse(response.data); } else if (Array.isArray(response.data)) { targets = response.data; } } catch (_e) { container.appendChild( E( "p", { style: "color: var(--color-danger, #dc3545);" }, "\u041E\u0448\u0438\u0431\u043A\u0430 \u0440\u0430\u0437\u0431\u043E\u0440\u0430 \u0441\u043F\u0438\u0441\u043A\u0430 \u0446\u0435\u043B\u0435\u0439." ) ); return; } if (targets.length === 0) { container.appendChild( E("p", {}, "\u0426\u0435\u043B\u0438 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.") ); return; } const activeSectionsBtn = renderButton({ text: "\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0435 \u043C\u0430\u0440\u0448\u0440\u0443\u0442\u044B", classNames: [ targetMode === "active" ? "cbi-button-action" : "cbi-button-neutral" ], onClick: () => loadTargets("active") }); activeSectionsBtn.style.fontSize = "12px"; activeSectionsBtn.style.padding = "4px 12px"; const allProfilesBtn = renderButton({ text: "\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B", classNames: [ targetMode === "all" ? "cbi-button-action" : "cbi-button-neutral" ], onClick: () => loadTargets("all") }); allProfilesBtn.style.fontSize = "12px"; allProfilesBtn.style.padding = "4px 12px"; const modeSwitcherBar = E( "div", { style: "display: flex; gap: 10px; align-items: center; margin-bottom: 14px; border-bottom: 1px solid var(--border-color, rgba(128,128,128,0.2)); padding-bottom: 10px;" }, [ E( "span", { style: "font-weight: 600; font-size: 13px; opacity: 0.9;" }, "\u0420\u0435\u0436\u0438\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438:" ), activeSectionsBtn, allProfilesBtn ] ); const sectionNames = Array.from(new Set(targets.map((t) => t.section))); let activeFilter = "ALL"; let searchQuery2 = ""; const totalStatEl = E( "b", { style: "font-size: 18px; display: block; margin-top: 2px;" }, `${targets.length}` ); const passedStatEl = E( "b", { style: "font-size: 18px; color: var(--color-success, #28a745); display: block; margin-top: 2px;" }, "0" ); const failedStatEl = E( "b", { style: "font-size: 18px; color: var(--color-danger, #dc3545); display: block; margin-top: 2px;" }, "0" ); const latencyStatEl = E( "b", { style: "font-size: 18px; color: var(--color-info, #17a2b8); display: block; margin-top: 2px;" }, "-" ); const createStatCard = (title, valueEl) => { return E( "div", { class: "cbi-value", style: "flex: 1 1 110px; min-width: 100px; padding: 8px 10px; border: 1px solid var(--border-color, rgba(128,128,128,0.25)); border-radius: 6px; text-align: center; background: var(--background-color-secondary, rgba(128,128,128,0.05)); margin: 0; box-sizing: border-box;" }, [ E( "small", { style: "display: block; opacity: 0.75; font-size: 11px;" }, title ), valueEl ] ); }; const statsBar = E( "div", { style: "display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; width: 100%; box-sizing: border-box;" }, [ createStatCard("\u0412\u0441\u0435\u0433\u043E \u0446\u0435\u043B\u0435\u0439", totalStatEl), createStatCard("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E", passedStatEl), createStatCard("\u041D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E", failedStatEl), createStatCard("\u0421\u0440. \u0437\u0430\u0434\u0435\u0440\u0436\u043A\u0430", latencyStatEl) ] ); const progressBarInner = E("div", { style: "width: 0%; height: 100%; background: var(--color-success, #28a745); transition: width 0.2s ease;" }); const progressBarContainer = E( "div", { style: "width: 100%; height: 6px; background: var(--border-color, rgba(128,128,128,0.2)); border-radius: 3px; overflow: hidden; margin-bottom: 14px; display: none;" }, [progressBarInner] ); const sectionSelect = E( "select", { class: "cbi-input-select", style: "padding: 4px 8px; font-size: 12px; border-radius: 4px; max-width: 240px;" }, [ E("option", { value: "ALL" }, `\u0412\u0441\u0435 \u0441\u0435\u0440\u0432\u0438\u0441\u044B (${targets.length})`), ...sectionNames.map((sec) => { const count = targets.filter((t) => t.section === sec).length; return E( "option", { value: sec }, `${formatSectionName(sec)} (${count})` ); }) ] ); sectionSelect.onchange = () => { activeFilter = sectionSelect.value; applyTableFilter(); }; const searchInput = E("input", { type: "text", placeholder: "\u041F\u043E\u0438\u0441\u043A \u0434\u043E\u043C\u0435\u043D\u0430 \u0438\u043B\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430...", class: "cbi-input-text", style: "width: 200px; padding: 4px 10px; font-size: 12px; border-radius: 4px;" }); searchInput.oninput = (e) => { const targetInput = e.target; searchQuery2 = (targetInput?.value || "").toLowerCase().trim(); applyTableFilter(); }; const toolbar = E( "div", { style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 10px; flex-wrap: wrap;" }, [ E( "div", { style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;" }, [ E( "span", { style: "font-size: 12px; opacity: 0.8;" }, "\u0424\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0441\u0435\u0440\u0432\u0438\u0441\u0443:" ), sectionSelect ] ), searchInput ] ); const tableHead = E("tr", { class: "tr cbi-section-table-titles" }, [ E( "th", { class: "th", style: "width: 28%; padding: 8px;" }, "\u0421\u0435\u0440\u0432\u0438\u0441 / \u0426\u0435\u043B\u044C" ), E( "th", { class: "th", style: "width: 17%; padding: 8px;" }, "\u041C\u0430\u0440\u0448\u0440\u0443\u0442" ), E( "th", { class: "th", style: "width: 20%; padding: 8px;" }, "IP / DNS" ), E( "th", { class: "th", style: "text-align: right; width: 7%; padding: 8px 4px;" }, "TCP" ), E( "th", { class: "th", style: "text-align: right; width: 7%; padding: 8px 4px;" }, "TLS" ), E( "th", { class: "th", style: "text-align: right; width: 7%; padding: 8px 4px;" }, "HTTP" ), E( "th", { class: "th", style: "text-align: center; width: 14%; padding: 8px 6px;" }, "\u0421\u0442\u0430\u0442\u0443\u0441" ) ]); const tableBody = E("tbody", {}); const table = E( "table", { class: "table cbi-section-table", style: "width: 100%; margin: 0; table-layout: fixed; box-sizing: border-box;" }, [ E( "thead", { style: "position: sticky; top: 0; background: var(--background-color-primary, #ffffff); z-index: 2;" }, [tableHead] ), tableBody ] ); const tableScrollWrapper = E( "div", { style: "max-height: 380px; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--border-color, rgba(128,128,128,0.25)); border-radius: 6px; width: 100%; box-sizing: border-box;" }, [table] ); const rowMap = []; targets.forEach((item, index) => { const rowClass = index % 2 === 0 ? "cbi-rowstyle-1" : "cbi-rowstyle-2"; const badge = renderStatusBadge("", false, true); const tr = E("tr", { class: `tr ${rowClass}` }, [ E( "td", { class: "td", style: "word-break: break-all; padding: 8px;" }, [ E( "span", { style: "font-weight: 600; font-size: 12px;" }, formatSectionName(item.section) ), E("br"), E("small", { style: "opacity: 0.8;" }, item.domain) ] ), E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [ E( "span", { class: "badge cbi-value-title", style: "font-size: 11px; padding: 2px 6px;" }, formatRouteType(item.route_type) ) ]), E("td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [ E("span", {}, "?"), E("br"), E("small", { style: "opacity: 0.6;" }, `-`) ]), E( "td", { class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" }, "-" ), E( "td", { class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" }, "-" ), E( "td", { class: "td", style: "text-align: right; font-size: 12px; padding: 8px 4px;" }, "-" ), E( "td", { class: "td", style: "text-align: center; padding: 8px 6px;" }, badge ) ]); rowMap.push({ target: item, tr }); tableBody.appendChild(tr); }); const applyTableFilter = () => { rowMap.forEach(({ target, tr }) => { const matchesSection = activeFilter === "ALL" || target.section === activeFilter; const matchesSearch = !searchQuery2 || target.domain.toLowerCase().includes(searchQuery2) || target.section.toLowerCase().includes(searchQuery2) || formatSectionName(target.section).toLowerCase().includes(searchQuery2); tr.style.display = matchesSection && matchesSearch ? "" : "none"; }); }; const updateSummaryStats = () => { let passed = 0; let failed2 = 0; let totalLat = 0; let countLat = 0; rowMap.forEach(({ result }) => { if (result) { if (result.success) passed++; else failed2++; const lat = result.http_ms || result.tls_ms || result.tcp_ms || 0; if (lat > 0) { totalLat += lat; countLat++; } } }); passedStatEl.textContent = `${passed}`; failedStatEl.textContent = `${failed2}`; latencyStatEl.textContent = countLat > 0 ? `${Math.round(totalLat / countLat)} ms` : "-"; }; container.appendChild(modeSwitcherBar); container.appendChild(statsBar); container.appendChild(progressBarContainer); container.appendChild(toolbar); container.appendChild(tableScrollWrapper); const footer = document.getElementById("tachyon-service-check-footer"); if (footer) { footer.innerHTML = ""; const customDomainInput = E("input", { type: "text", id: "custom-domain-input", placeholder: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u043E\u043C\u0435\u043D \u0438\u043B\u0438 IP (\u043D\u0430\u043F\u0440. example.com)...", class: "cbi-input-text", style: "width: 270px; font-size: 12px;" }); const customBtn = renderButton({ text: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u043E\u043C\u0435\u043D", classNames: ["cbi-button-neutral"], onClick: async () => { const domain = customDomainInput.value.trim(); if (!domain) return; customBtn.disabled = true; customBtn.textContent = "..."; try { const cRes = await callBaseMethod( Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK, ["check-custom", domain] ); if (cRes && cRes.success) { const cResults = typeof cRes.data === "string" ? JSON.parse(cRes.data) : cRes.data || []; if (cResults && cResults.length > 0) { const cItem = cResults[0]; const cBadge = renderStatusBadge(cItem.status_class); const customTr = E("tr", { class: "tr cbi-rowstyle-1" }, [ E( "td", { class: "td", style: "word-break: break-all; padding: 8px;" }, [ E( "span", { style: "font-weight: 600;" }, formatSectionName(cItem.section) ), E("br"), E("small", {}, cItem.domain) ] ), E( "td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [ E( "span", { class: "badge cbi-value-title", style: "font-size: 11px; padding: 2px 6px;" }, formatRouteType(cItem.route_type) ) ] ), E( "td", { class: "td", style: "font-size: 12px; padding: 8px;" }, [ cItem.ip || "?", E("br"), E("small", {}, `${cItem.dns_ms}ms`) ] ), E( "td", { class: "td", style: "text-align: right; padding: 8px 4px;" }, cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : "-" ), E( "td", { class: "td", style: "text-align: right; padding: 8px 4px;" }, cItem.tls_ms > 0 ? `${cItem.tls_ms}` : "-" ), E( "td", { class: "td", style: "text-align: right; padding: 8px 4px;" }, cItem.http_ms > 0 ? `${cItem.http_ms}` : "-" ), E( "td", { class: "td", style: "text-align: center; padding: 8px 6px;" }, cBadge ) ]); tableBody.insertBefore(customTr, tableBody.firstChild); rowMap.unshift({ target: { section: "Custom", route_type: "auto", domain: cItem.domain }, tr: customTr, result: cItem }); updateSummaryStats(); customDomainInput.value = ""; } } } catch (_e) { } finally { customBtn.disabled = false; customBtn.textContent = "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u043E\u043C\u0435\u043D"; } } }); const leftWrap = E( "div", { style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;" }, [customDomainInput, customBtn] ); const checkAllBtn = renderButton({ text: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u0441\u0435", classNames: ["cbi-button-action"], onClick: async () => { checkAllBtn.disabled = true; checkAllBtn.textContent = "\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430..."; progressBarContainer.style.display = "block"; progressBarInner.style.width = "0%"; for (let i = 0; i < rowMap.length; i++) { const itemObj = rowMap[i]; const { target, tr } = itemObj; progressBarInner.style.width = `${Math.round((i + 1) / rowMap.length * 100)}%`; const badgeCell = tr.childNodes[6]; badgeCell.innerHTML = ""; badgeCell.appendChild(renderStatusBadge("", true)); try { const cRes = await callBaseMethod( Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK, ["check-domain", JSON.stringify(target)] ); if (cRes && cRes.success) { const cResults = typeof cRes.data === "string" ? JSON.parse(cRes.data) : cRes.data || []; if (cResults && cResults.length > 0) { const cItem = cResults[0]; itemObj.result = cItem; const cBadge = renderStatusBadge(cItem.status_class); tr.childNodes[2].innerHTML = ""; tr.childNodes[2].appendChild( document.createTextNode(cItem.ip || "?") ); tr.childNodes[2].appendChild(E("br")); tr.childNodes[2].appendChild( E( "small", { style: "opacity: 0.6;" }, `${cItem.dns_ms}ms` ) ); tr.childNodes[3].textContent = cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : "-"; tr.childNodes[4].textContent = cItem.tls_ms > 0 ? `${cItem.tls_ms}` : "-"; tr.childNodes[5].textContent = cItem.http_ms > 0 ? `${cItem.http_ms}` : "-"; badgeCell.innerHTML = ""; badgeCell.appendChild(cBadge); } } } catch (_e) { badgeCell.innerHTML = ""; badgeCell.appendChild(renderStatusBadge("Failed")); } updateSummaryStats(); } checkAllBtn.textContent = "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0441\u043D\u043E\u0432\u0430"; checkAllBtn.disabled = false; } }); const closeBtn = renderButton({ text: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C", onClick: () => ui.hideModal() }); const rightWrap = E("div", { style: "display: flex; gap: 8px;" }, [ checkAllBtn, closeBtn ]); footer.appendChild(leftWrap); footer.appendChild(rightWrap); } }).catch(() => { container.innerHTML = ""; container.appendChild( E( "p", { style: "color: var(--color-danger, #dc3545);" }, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u043E\u0432." ) ); }); }; loadTargets("active"); } // src/tachyon/tabs/diagnostic/partials/renderAiChatModal.ts var chatHistory = [ { sender: "assistant", text: _( "Hello! I am Tachyon AI Assistant. How can I help you today? Ask me about domain blocks, diagnostics, or router settings." ), timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) } ]; function renderAiChatModal() { const messageListContainer = E("div", { style: "height: 360px; overflow-y: auto; padding: 12px; background: var(--background-color-secondary, rgba(0,0,0,0.2)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 8px; display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; width: 100%; box-sizing: border-box;" }); const renderMessages = () => { messageListContainer.innerHTML = ""; chatHistory.forEach((msg) => { const isUser = msg.sender === "user"; const bubble = E( "div", { style: `max-width: 82%; align-self: ${isUser ? "flex-end" : "flex-start"}; background: ${isUser ? "#007bff" : "var(--background-color-primary, #2a2a2a)"}; color: #fff; padding: 8px 12px; border-radius: 12px; font-size: 13px; border: 1px solid ${isUser ? "#0056b3" : "var(--border-color, rgba(255,255,255,0.15))"}; line-height: 1.4; word-break: break-word;` }, [ E("div", {}, msg.text), E( "small", { style: "display: block; opacity: 0.65; text-align: right; font-size: 10px; margin-top: 4px;" }, msg.timestamp ) ] ); messageListContainer.appendChild(bubble); }); messageListContainer.scrollTop = messageListContainer.scrollHeight; }; renderMessages(); const chatInput = E("input", { type: "text", placeholder: _("Ask Tachyon AI Assistant a question..."), class: "cbi-input-text", style: "flex: 1 1 auto; min-width: 0; font-size: 13px; padding: 6px 10px; border-radius: 6px; box-sizing: border-box;" }); const sendBtn = renderButton({ text: _("Send"), classNames: ["cbi-button-action"], onClick: () => handleSend() }); const handleSend = async (queryText) => { const text = (queryText || chatInput.value).trim(); if (!text) return; const userMsg = { sender: "user", text, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }; chatHistory.push(userMsg); chatInput.value = ""; renderMessages(); const typingMsg = { sender: "assistant", text: "\u{1F916} " + _("Thinking..."), timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }; chatHistory.push(typingMsg); renderMessages(); try { const res = await callBaseMethod( Tachyon.AvailableMethods.AI_DOCTOR ); chatHistory.pop(); let answerText = _("Failed to receive response from AI service."); if (res && res.success) { if (typeof res.data === "string") { try { const parsed = JSON.parse(res.data); answerText = parsed.summary || parsed.message || parsed.raw || res.data; } catch (_e) { answerText = res.data; } } else if (typeof res.data === "object" && res.data !== null && "message" in res.data) { answerText = String(res.data.message); } } chatHistory.push({ sender: "assistant", text: answerText, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }); } catch (e) { chatHistory.pop(); const errMsg = e instanceof Error ? e.message : _("Server unavailable"); chatHistory.push({ sender: "assistant", text: "\u274C " + _("AI service error") + ": " + errMsg, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }); } renderMessages(); }; chatInput.onkeydown = (e) => { if (e.key === "Enter") handleSend(); }; const quickPrompts = [ { label: "\u{1FA7A} " + _("Check YouTube"), query: "Check YouTube availability" }, { label: "\u{1F50D} " + _("Why Discord fails?"), query: "Why Discord fails?" }, { label: "\u{1F6E0}\uFE0F " + _("Full System Diagnostic"), query: "Run full system diagnostic" } ]; const quickPromptsBar = E( "div", { style: "display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; width: 100%; box-sizing: border-box;" }, quickPrompts.map((qp) => { const btn = E( "button", { type: "button", class: "cbi-button cbi-button-neutral", style: "font-size: 11px; padding: 2px 8px; border-radius: 10px;" }, qp.label ); btn.onclick = () => handleSend(qp.query); return btn; }) ); const inputToolbar = E( "div", { style: "display: flex; gap: 8px; align-items: center; width: 100%; box-sizing: border-box;" }, [chatInput, sendBtn] ); const modalWrapper = E( "div", { style: "width: 100%; max-width: 680px; box-sizing: border-box;" }, [quickPromptsBar, messageListContainer, inputToolbar] ); const modalContent = E( "div", { style: "width: 100%; max-width: 680px; box-sizing: border-box;" }, [ modalWrapper, E( "div", { style: "margin-top: 12px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border-color, rgba(255,255,255,0.15)); padding-top: 10px;" }, [ renderButton({ text: _("Close"), onClick: () => ui.hideModal() }) ] ) ] ); ui.showModal(_("AI Chat & Tachyon Assistant"), modalContent); } // src/tachyon/tabs/diagnostic/partials/renderSystemInfo.ts function renderSystemInfo({ items }) { return E( "div", { class: "tachyon_diagnostic-page__right-bar__system-info" }, [ E( "b", { class: "tachyon_diagnostic-page__right-bar__system-info__title" }, _("System information") ), ...items.map((item) => { const tagClass = [ "tachyon_diagnostic-page__right-bar__system-info__row__tag", ...insertIf(item.tag?.kind === "neutral", [ "tachyon_diagnostic-page__right-bar__system-info__row__tag--neutral" ]), ...insertIf(item.tag?.kind === "warning", [ "tachyon_diagnostic-page__right-bar__system-info__row__tag--warning" ]), ...insertIf(item.tag?.kind === "success", [ "tachyon_diagnostic-page__right-bar__system-info__row__tag--success" ]) ].filter(Boolean).join(" "); return E( "div", { class: "tachyon_diagnostic-page__right-bar__system-info__row" }, [ E("b", {}, item.key), E("div", {}, [ E("span", {}, item.value), E("span", { class: tagClass }, item?.tag?.label) ]) ] ); }) ] ); } // src/helpers/normalizeCompiledVersion.ts function normalizeCompiledVersion(version, commitSha) { if (!version || version.includes("COMPILED")) { return "dev"; } if (commitSha && commitSha !== "unknown" && !commitSha.includes("COMPILED")) { const shortSha = commitSha.substring(0, 8); if (shortSha && !version.includes(shortSha)) { return `${version} (${shortSha})`; } } return version; } // src/tachyon/tabs/diagnostic/partials/renderWikiDisclaimer.ts function renderWikiDisclaimer(kind) { const iconWrap = E("span", { class: "tachyon_diagnostic-page__right-bar__wiki__icon" }); iconWrap.appendChild(renderBookOpenTextIcon24()); const className = [ "tachyon_diagnostic-page__right-bar__wiki", ...insertIf(kind === "error", [ "tachyon_diagnostic-page__right-bar__wiki--error" ]), ...insertIf(kind === "warning", [ "tachyon_diagnostic-page__right-bar__wiki--warning" ]) ].join(" "); return E("div", { class: className }, [ E("div", { class: "tachyon_diagnostic-page__right-bar__wiki__content" }, [ iconWrap, E("div", { class: "tachyon_diagnostic-page__right-bar__wiki__texts" }, [ E("b", {}, _("Troubleshooting")), E("div", {}, _("Do not panic, everything can be fixed, just...")) ]) ]), renderButton({ classNames: ["cbi-button-save"], text: _("Open Project Page"), onClick: () => window.open( "https://github.com/Dushnilin/tachyon#readme", "_blank", "noopener,noreferrer" ) }) ]); } // src/tachyon/tabs/diagnostic/checks/runSectionsCheck.ts function getSubscriptionLatencyState(latencyValues) { const hasAvailableLatency = latencyValues.some((item) => Boolean(item)); const hasUnavailableLatency = latencyValues.some((item) => !item); if (!hasAvailableLatency) { return "error"; } if (hasUnavailableLatency) { return "warning"; } return "success"; } async function runSectionsCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS; updateCheckStore({ order, code, title, description: _("Checking, please wait"), state: "loading", items: [] }); const sections = await getDashboardSections({ includeSubscriptionCopyState: false }); if (!sections.success) { updateCheckStore({ order, code, title, description: _("Cannot receive checks result"), state: "error", items: [] }); throw new Error("Rule outbounds checks failed"); } const items = []; for (const section of sections.data) { async function getLatency() { if (section.withTagSelect) { const selectedOutbound2 = section.outbounds.find((item) => item.selected) ?? section.outbounds.find( (item) => item.type?.toLowerCase() === "urltest" ) ?? section.outbounds[0]; const isSubscription = section.proxyConfigType === "subscription"; if (selectedOutbound2?.code) { const latencyProxy2 = await TachyonShellMethods.getClashApiProxyLatency( selectedOutbound2.code, section.latencyTestTimeout ); const proxySuccess = latencyProxy2.success && !latencyProxy2.data.message; if (proxySuccess) { return { state: "success", latency: `[${selectedOutbound2.displayName ?? ""}] ${latencyProxy2.data.delay}ms` }; } return { state: "error", latency: `[${selectedOutbound2.displayName ?? ""}] ${_("Not responding")}` }; } const latencyGroup = await TachyonShellMethods.getClashApiGroupLatency( section.code ); const success2 = latencyGroup.success && !latencyGroup.data.message; if (success2) { const latencyValues = Object.values(latencyGroup.data); const sectionState = isSubscription ? getSubscriptionLatencyState(latencyValues) : "success"; const selectedProxyDelay = latencyGroup.data?.[selectedOutbound2?.code ?? ""]; if (selectedProxyDelay) { return { state: sectionState, latency: `[${selectedOutbound2?.displayName ?? ""}] ${selectedProxyDelay}ms` }; } return { state: "error", latency: `[${selectedOutbound2?.displayName ?? ""}] ${_("Not responding")}` }; } return { state: "error", latency: _("Not responding") }; } if (section.serviceStatus) { const s = section.serviceStatus; if (s.ready) { return { state: "success", latency: _("Running") }; } if (s.conflict) { return { state: "error", latency: _("Conflict") }; } if (s.configured) { return { state: "warning", latency: _("Stopped") }; } return { state: "error", latency: _("Not configured") }; } const selectedOutbound = section.outbounds[0]; const latencyProxy = await TachyonShellMethods.getClashApiProxyLatency( section.code, section.latencyTestTimeout ); const success = latencyProxy.success && !latencyProxy.data.message; if (success) { return { state: "success", latency: `${latencyProxy.data.delay} ms` }; } if (section.action === "vpn" && selectedOutbound?.runtimeAvailable) { return { state: "warning", latency: `[${selectedOutbound.displayName || section.code}] ${_("Connectivity probe failed")}` }; } return { state: "error", latency: _("Not responding") }; } const { latency, state: state2 } = await getLatency(); items.push({ state: state2, key: section.displayName, value: latency }); } const allGood = items.every((item) => item.state === "success"); const atLeastOneGood = items.some((item) => item.state !== "error"); const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, code, title, description, state, items }); if (!atLeastOneGood) { throw new Error("Rule outbounds checks failed"); } } // src/tachyon/tabs/diagnostic/serviceTransition.ts function isServiceTransitionStatus(status) { return ["starting", "stopping", "restarting", "reloading"].includes(status); } function getServiceTransition(status) { return { starting: status === "starting", stopping: status === "stopping", restarting: status === "restarting" || status === "reloading" }; } function hasLocalMutatingServiceActionLoading(actions) { return actions.restart.loading || actions.start.loading || actions.stop.loading || actions.enable.loading || actions.disable.loading; } function shouldSkipServicesInfoAutoRefresh({ force, localMutatingActionLoading }) { return !force && localMutatingActionLoading; } function shouldResetDiagnosticsChecks({ resetChecks, diagnosticsRunLoading }) { return resetChecks && !diagnosticsRunLoading; } function shouldDisableDiagnosticRunAction({ providerInfoLoaded, servicesInfoLoading, tachyonRunning, mutatingServiceActionLoading }) { return !providerInfoLoaded || servicesInfoLoading || !tachyonRunning || mutatingServiceActionLoading; } function hasComponentActionLoading(actions) { return Object.values(actions).some((action) => action.loading); } function getAvailableActionsDisabledState({ servicesInfoLoading, mutatingServiceActionLoading, componentActionLoading }) { return { serviceControlsDisabled: servicesInfoLoading || mutatingServiceActionLoading || componentActionLoading, utilityActionsDisabled: mutatingServiceActionLoading || componentActionLoading, viewLogsDisabled: false }; } function shouldShowRestartAction({ tachyonRunning, restartLoading, startLoading, stopLoading }) { return restartLoading || tachyonRunning && !startLoading && !stopLoading; } function shouldShowStartAction({ tachyonRunning, restartLoading, startLoading, stopLoading }) { return startLoading || !restartLoading && !tachyonRunning && !stopLoading; } function shouldShowStopAction({ tachyonRunning, restartLoading, startLoading, stopLoading }) { return stopLoading || restartLoading || tachyonRunning && !startLoading; } // src/tachyon/tabs/diagnostic/diagnosticRunPersistence.ts var DIAGNOSTIC_RUN_STORAGE_KEY = "tachyon:diagnostic-run:v1"; var DIAGNOSTIC_RUN_TTL_MS = 30 * 60 * 1e3; var CHECK_STATES = ["loading", "warning", "success", "error", "skipped"]; var CHECK_ITEM_STATES = ["error", "warning", "success"]; function getSessionStorage3() { if (typeof window === "undefined") { return null; } try { return window.sessionStorage; } catch { return null; } } function isRecord(value) { return Boolean(value) && typeof value === "object"; } function isOptionalBoolean(value) { return value === void 0 || typeof value === "boolean"; } function isDiagnosticsProviderOptions(value) { if (!isRecord(value)) { return false; } return isOptionalBoolean(value.includeZapret) && isOptionalBoolean(value.includeZapret2) && isOptionalBoolean(value.includeByedpi) && isOptionalBoolean(value.includeInbounds); } function isDiagnosticCheckItem(value) { return isRecord(value) && CHECK_ITEM_STATES.includes(String(value.state)) && typeof value.key === "string" && typeof value.value === "string"; } function isDiagnosticCheck(value) { return isRecord(value) && Number.isFinite(value.order) && Object.values(DIAGNOSTICS_CHECKS).includes( value.code ) && typeof value.title === "string" && typeof value.description === "string" && CHECK_STATES.includes(String(value.state)) && Array.isArray(value.items) && value.items.every(isDiagnosticCheckItem); } function isPersistedDiagnosticRun(value) { if (!isRecord(value)) { return false; } return typeof value.nextRunnerIndex === "number" && Number.isInteger(value.nextRunnerIndex) && value.nextRunnerIndex >= 0 && isDiagnosticsProviderOptions(value.providerOptions) && Array.isArray(value.diagnosticsChecks) && value.diagnosticsChecks.every(isDiagnosticCheck) && Number.isFinite(value.updatedAt); } function isExpired(run, now = Date.now()) { return now - run.updatedAt > DIAGNOSTIC_RUN_TTL_MS; } function readPersistedDiagnosticRun(storage = getSessionStorage3()) { if (!storage) { return null; } try { const parsed = JSON.parse( storage.getItem(DIAGNOSTIC_RUN_STORAGE_KEY) || "null" ); if (!isPersistedDiagnosticRun(parsed) || isExpired(parsed)) { storage.removeItem(DIAGNOSTIC_RUN_STORAGE_KEY); return null; } return parsed; } catch { storage.removeItem(DIAGNOSTIC_RUN_STORAGE_KEY); return null; } } function savePersistedDiagnosticRun(run, storage = getSessionStorage3()) { if (!storage) { return; } try { storage.setItem( DIAGNOSTIC_RUN_STORAGE_KEY, JSON.stringify({ ...run, updatedAt: Date.now() }) ); } catch { } } function clearPersistedDiagnosticRun(storage = getSessionStorage3()) { if (!storage) { return; } try { storage.removeItem(DIAGNOSTIC_RUN_STORAGE_KEY); } catch { } } // src/tachyon/tabs/diagnostic/helpers/maskDiagnostics.ts var MASKED_VALUE = "*******"; var SING_BOX_MASKED_KEYS = /* @__PURE__ */ new Set([ "auth_key", "control_url", "exit_node", "hostname", "listen", "listen_port", "username", "uuid", "server", "server_name", "secret", "password", "private_key", "public_key", "short_id", "fingerprint", "server_port", "server_ports", "advertise_routes", "domain", "domain_suffix", "domain_keyword", "domain_regex", "ip_cidr", "source_ip_cidr" ]); var TACHYON_MASK_AFTER_TOKEN = [ "option proxy_string", "option hwid", "option subscription_url", "list subscription_urls", "list urltest_proxy_links", "list selector_proxy_links", "list server_users", "option server_uuid", "option server_username", "option server_password", "option mtproto_secret", "option hysteria2_obfs_password", "option reality_private_key", "option reality_public_key", "option reality_short_id", "list reality_short_id", "option yacd_secret_key" ]; var TACHYON_MASK_AFTER_TOKEN_SPACE = [ "option outbound_json", "list domain", "list domain_suffix", "list domain_keyword", "list domain_regex", "list ip_cidr", "list source_ip_cidr", "list fully_routed_ips", "list excluded_ips", "option dns_server", "option bootstrap_dns_server", "list dns_server", "list bootstrap_dns_server", "option listen", "option listen_port", "option public_host", "option mtproto_faketls", "option mtproto_domain_fronting_ip", "option tls_server_name", "option reality_handshake_server", "option reality_handshake_server_port", "option transport_host", "list transport_hosts", "option tailscale_auth_key", "option tailscale_control_url", "option tailscale_hostname", "list tailscale_advertise_routes", "option tailscale_ephemeral", "option tailscale_exit_node", "option tailscale_exit_node_allow_lan_access", "option mixed_proxy_username", "option mixed_proxy_password", "option ipaddr", "option netmask", "option gateway", "option username", "option password", "option private_key", "option awg_private_key", "option bot_token", "option admin_ids", "option url" ]; function isRecord2(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function isSpaceChar(value) { return value === " " || value === " " || value === "\r" || value === "\n"; } function maskAfterToken(line, token) { const position = line.indexOf(token); return position < 0 ? line : `${line.slice(0, position)}${token} '${MASKED_VALUE}'`; } function maskAfterTokenSpace(line, token) { const position = line.indexOf(token); if (position < 0) { return line; } const spacePosition = position + token.length; if (spacePosition >= line.length || !isSpaceChar(line.slice(spacePosition, spacePosition + 1))) { return line; } return `${line.slice(0, spacePosition + 1)}'${MASKED_VALUE}'`; } function maskOptionPath(line, token) { const position = line.indexOf(token); if (position < 0) { return line; } const slashOffset = line.slice(position + token.length).indexOf("/"); if (slashOffset < 0) { return line; } const slash = slashOffset + position + token.length; const quoteOffset = line.slice(slash + 1).indexOf("'"); if (quoteOffset < 0) { return line; } const quote = quoteOffset + slash + 1; return `${line.slice(0, slash)}/*******'${line.slice(quote + 1)}`; } function maskGlobalCheckLine(line) { let maskedLine = line; for (const token of TACHYON_MASK_AFTER_TOKEN) { maskedLine = maskAfterToken(maskedLine, token); } for (const token of TACHYON_MASK_AFTER_TOKEN_SPACE) { maskedLine = maskAfterTokenSpace(maskedLine, token); } maskedLine = maskOptionPath(maskedLine, "option dns_server '"); maskedLine = maskOptionPath(maskedLine, "list dns_server '"); return maskedLine; } function maskMultilineContinuation(line) { const leadingSpace = line.match(/^\s*/)?.[0] ?? ""; const hasClosingQuote = line.includes("'"); return `${leadingSpace}${MASKED_VALUE}${hasClosingQuote ? "'" : ""}`; } function maskSingBoxConfigValue(value) { if (Array.isArray(value)) { return value.map((item) => maskSingBoxConfigValue(item)); } if (isRecord2(value)) { return Object.fromEntries( Object.entries(value).map(([key, item]) => [ key, SING_BOX_MASKED_KEYS.has(key) ? MASKED_VALUE : maskSingBoxConfigValue(item) ]) ); } return value; } function stringifySingBoxConfig(value) { return typeof value === "string" ? value : JSON.stringify(value, null, 2); } function formatMaskedSingBoxConfig(value) { if (typeof value === "string") { try { return JSON.stringify(maskSingBoxConfigValue(JSON.parse(value)), null, 2); } catch (_error) { return value; } } return JSON.stringify(maskSingBoxConfigValue(value), null, 2); } function maskGlobalCheckText(text = "") { let inMaskedMultiline = false; return `${text}`.split("\n").map((line) => { if (inMaskedMultiline) { if (line.includes("'")) { inMaskedMultiline = false; } return maskMultilineContinuation(line); } const maskedLine = maskGlobalCheckLine(line); if (line.includes("option outbound_json")) { const firstQuote = line.indexOf("'"); if (firstQuote >= 0 && line.slice(firstQuote + 1).indexOf("'") < 0) { inMaskedMultiline = true; } } return maskedLine; }).join("\n"); } // src/tachyon/tabs/diagnostic/initController.ts var SERVICE_STATUS_REFRESH_INTERVAL_MS = 2e3; var SERVICE_ACTION_STATUS_TIMEOUT_MS = 45e3; var latestProviderInfoRequestId = 0; var diagnosticLifecycleRegistered = false; var diagnosticControllerInitialized = false; var diagnosticMounted = false; var diagnosticMountId = 0; var diagnosticCompletedWhileHidden = false; var servicesInfoStateUnsubscribe = null; var servicesInfoRefreshPromise = null; var followedServiceActionJobs = /* @__PURE__ */ new Set(); var handledServiceActionJobs = /* @__PURE__ */ new Set(); function sleep2(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getDiagnosticsProviderOptions(systemInfo = store.get().diagnosticsSystemInfo) { return { includeZapret: Boolean(systemInfo.zapret_installed), includeZapret2: Boolean(systemInfo.zapret2_installed), includeByedpi: Boolean(systemInfo.byedpi_installed), includeInbounds: systemInfo.server_inbounds_enabled_count > 0 }; } function getNotRunningDiagnosticsChecks() { return getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions() ); } function resetDiagnosticsChecks() { store.set({ diagnosticsChecks: getNotRunningDiagnosticsChecks() }); } function setDiagnosticActionLoading(action, loading2, local = false) { if (local || !loading2) { setLocalServiceAction(action, loading2 && local); } const diagnosticsActions = store.get().diagnosticsActions; store.set({ diagnosticsActions: { ...diagnosticsActions, [action]: { loading: loading2 } } }); } function isDiagnosticMountActive(mountId = diagnosticMountId) { return diagnosticMounted && diagnosticMountId === mountId; } function isLocalMutatingServiceActionLoading() { const actions = store.get().diagnosticsActions; return hasLocalMutatingServiceActionLoading(actions); } function isMutatingServiceActionLoading() { return isLocalMutatingServiceActionLoading() || isServiceTransitionStatus(store.get().servicesInfoWidget.data.tachyonStatus); } function getTachyonStatusText(running, enabled) { if (running) { return enabled ? "running & enabled" : "running but disabled"; } return enabled ? "stopped but enabled" : "stopped & disabled"; } function setDisplayedTachyonRunning(running) { const servicesInfoWidget = store.get().servicesInfoWidget; const enabled = Boolean(servicesInfoWidget.data.tachyonEnabled); store.set({ servicesInfoWidget: { ...servicesInfoWidget, loading: false, data: { ...servicesInfoWidget.data, tachyonRunning: running ? 1 : 0, tachyonStatus: getTachyonStatusText(running, enabled) } } }); } async function refreshDiagnosticServicesInfo({ force = false, mountId = diagnosticMountId, allowInactive = false } = {}) { if (!allowInactive && !isDiagnosticMountActive(mountId)) { return; } if (shouldSkipServicesInfoAutoRefresh({ force, localMutatingActionLoading: isLocalMutatingServiceActionLoading() })) { return; } if (servicesInfoRefreshPromise) { return servicesInfoRefreshPromise; } const promise = fetchServicesInfo().then((uiState) => { followServiceActionsFromUiState(uiState); }).catch((error) => { logger.error( "[DIAGNOSTIC]", "refreshDiagnosticServicesInfo failed", error ); }).finally(() => { if (servicesInfoRefreshPromise === promise) { servicesInfoRefreshPromise = null; } }); servicesInfoRefreshPromise = promise; return promise; } async function waitForTachyonRunningState(expectedRunning) { const startedAt = Date.now(); while (Date.now() - startedAt < SERVICE_ACTION_STATUS_TIMEOUT_MS) { await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); const tachyonRunning = Boolean( store.get().servicesInfoWidget.data.tachyonRunning ); if (tachyonRunning === expectedRunning) { return true; } await sleep2(SERVICE_STATUS_REFRESH_INTERVAL_MS); } return false; } function startServiceActionStateWatcher() { if (servicesInfoStateUnsubscribe) { return; } servicesInfoStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (diagnosticMounted) { followServiceActionsFromUiState(uiState); } }); } function stopServiceActionStateWatcher() { if (!servicesInfoStateUnsubscribe) { return; } servicesInfoStateUnsubscribe(); servicesInfoStateUnsubscribe = null; } function isVisibleServiceRuntimeAction(action) { return action === "restart" || action === "start" || action === "stop"; } function setServiceActionStateLoading(state, loading2) { if (!isVisibleServiceRuntimeAction(state.action)) { return; } setDiagnosticActionLoading(state.action, loading2); } async function followServiceActionState(state) { const jobId = state.job_id; if (!jobId || followedServiceActionJobs.has(jobId)) { return; } if (!state.running && handledServiceActionJobs.has(jobId)) { return; } followedServiceActionJobs.add(jobId); if (state.running) { setServiceActionStateLoading(state, true); } try { if (state.running) { await TachyonShellMethods.waitServiceActionJob(jobId); } } catch (error) { logger.error("[DIAGNOSTIC]", "followServiceActionState failed", error); } finally { handledServiceActionJobs.add(jobId); setServiceActionStateLoading(state, false); await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); void TachyonShellMethods.uiActionAck("service", jobId); followedServiceActionJobs.delete(jobId); resetDiagnosticsChecks(); } } function followServiceActionsFromUiState(uiState) { if (!uiState) { return; } for (const action of uiState.actions.service || []) { if (action.job_id) { void followServiceActionState(action); } } } async function fetchSystemInfo() { const systemInfo = await ensureSystemInfo(); if (store.get().diagnosticsRunAction.loading) { return; } store.set({ diagnosticsChecks: getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions(systemInfo) ) }); } async function fetchDiagnosticsProviderInfo({ resetChecks = true } = {}) { const requestId = ++latestProviderInfoRequestId; try { const uiState = await refreshRuntimeUiState({ force: true }); if (requestId !== latestProviderInfoRequestId) { return; } if (uiState) { const currentSystemInfo2 = store.get().diagnosticsSystemInfo; const nextSystemInfo2 = normalizeSingBoxVariantFields({ ...currentSystemInfo2, providerInfoLoaded: true, sing_box_extended: uiState.capabilities.sing_box_extended, sing_box_tiny: uiState.capabilities.sing_box_tiny, sing_box_compressed: uiState.capabilities.sing_box_compressed, sing_box_lx: uiState.capabilities.sing_box_lx, sing_box_tailscale: uiState.capabilities.sing_box_tailscale, zapret_installed: uiState.capabilities.zapret_installed, zapret2_installed: uiState.capabilities.zapret2_installed, byedpi_installed: uiState.capabilities.byedpi_installed, server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count }); if (!nextSystemInfo2.zapret_installed) { nextSystemInfo2.zapret_version = "not installed"; } if (!nextSystemInfo2.zapret2_installed) { nextSystemInfo2.zapret2_version = "not installed"; } if (!nextSystemInfo2.byedpi_installed) { nextSystemInfo2.byedpi_version = "not installed"; } const nextState2 = { diagnosticsSystemInfo: nextSystemInfo2 }; if (shouldResetDiagnosticsChecks({ resetChecks, diagnosticsRunLoading: store.get().diagnosticsRunAction.loading })) { nextState2.diagnosticsChecks = getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions(nextSystemInfo2) ); } store.set(nextState2); return; } const [zapretRuntime, zapret2Runtime, byedpiRuntime, inboundsConfig] = await Promise.all([ TachyonShellMethods.checkZapretRuntime(), TachyonShellMethods.checkZapret2Runtime(), TachyonShellMethods.checkByedpiRuntime(), TachyonShellMethods.checkInboundsConfig() ]); if (requestId !== latestProviderInfoRequestId) { return; } const currentSystemInfo = store.get().diagnosticsSystemInfo; const nextSystemInfo = { ...currentSystemInfo, providerInfoLoaded: true, zapret_installed: zapretRuntime.success ? zapretRuntime.data.zapret_installed : currentSystemInfo.zapret_installed, zapret2_installed: zapret2Runtime.success ? zapret2Runtime.data.zapret2_installed : currentSystemInfo.zapret2_installed, byedpi_installed: byedpiRuntime.success ? byedpiRuntime.data.byedpi_installed : currentSystemInfo.byedpi_installed, server_inbounds_enabled_count: inboundsConfig.success ? inboundsConfig.data.enabled_count : -1 }; if (!zapretRuntime.success) { logger.error("[DIAGNOSTIC]", "fetchZapretRuntime failed", zapretRuntime); } if (!zapret2Runtime.success) { logger.error( "[DIAGNOSTIC]", "fetchZapret2Runtime failed", zapret2Runtime ); } if (!byedpiRuntime.success) { logger.error("[DIAGNOSTIC]", "fetchByedpiRuntime failed", byedpiRuntime); } if (!inboundsConfig.success) { logger.error( "[DIAGNOSTIC]", "fetchInboundsConfig failed", inboundsConfig ); } if (!nextSystemInfo.zapret_installed) { nextSystemInfo.zapret_version = "not installed"; } if (!nextSystemInfo.zapret2_installed) { nextSystemInfo.zapret2_version = "not installed"; } if (!nextSystemInfo.byedpi_installed) { nextSystemInfo.byedpi_version = "not installed"; } const nextState = { diagnosticsSystemInfo: nextSystemInfo }; if (shouldResetDiagnosticsChecks({ resetChecks, diagnosticsRunLoading: store.get().diagnosticsRunAction.loading })) { nextState.diagnosticsChecks = getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions(nextSystemInfo) ); } store.set(nextState); } catch (error) { logger.error("[DIAGNOSTIC]", "fetchDiagnosticsProviderInfo failed", error); if (requestId === latestProviderInfoRequestId) { const currentSystemInfo = store.get().diagnosticsSystemInfo; store.set({ diagnosticsSystemInfo: { ...currentSystemInfo, providerInfoLoaded: true, server_inbounds_enabled_count: -1 } }); } } } function renderDiagnosticsChecks() { logger.debug("[DIAGNOSTIC]", "renderDiagnosticsChecks"); const diagnosticsChecks = [...store.get().diagnosticsChecks].sort( (a, b) => a.order - b.order ); const container = document.getElementById("tachyon_diagnostic-page-checks"); const renderedDiagnosticsChecks = diagnosticsChecks.map( (check) => renderCheckSection(check) ); return preserveScrollForPage(() => { container.replaceChildren(...renderedDiagnosticsChecks); }); } function renderDiagnosticRunActionWidget() { logger.debug("[DIAGNOSTIC]", "renderDiagnosticRunActionWidget"); const { loading: loading2 } = store.get().diagnosticsRunAction; const providerInfoLoaded = store.get().diagnosticsSystemInfo.providerInfoLoaded; const servicesInfoWidget = store.get().servicesInfoWidget; const tachyonRunning = Boolean(servicesInfoWidget.data.tachyonRunning); const container = document.getElementById( "tachyon_diagnostic-page-run-check" ); const renderedAction = renderRunAction({ loading: loading2, disabled: shouldDisableDiagnosticRunAction({ providerInfoLoaded, servicesInfoLoading: servicesInfoWidget.loading, tachyonRunning, mutatingServiceActionLoading: isMutatingServiceActionLoading() }), click: () => runChecks() }); return preserveScrollForPage(() => { container.replaceChildren(renderedAction); }); } async function handleServiceRuntimeAction({ action, expectedRunning, optimisticRunning }) { setDiagnosticActionLoading(action, true, true); let jobId = ""; let ownsJobFollow = false; let delegatedToWatcher = false; if (optimisticRunning !== void 0) { setDisplayedTachyonRunning(optimisticRunning); } try { const startResponse = await TachyonShellMethods.serviceActionStart(action); if (!startResponse.success) { throw new Error(startResponse.error); } jobId = startResponse.data.job_id; if (followedServiceActionJobs.has(jobId)) { delegatedToWatcher = true; return; } followedServiceActionJobs.add(jobId); ownsJobFollow = true; const result = await TachyonShellMethods.waitServiceActionJob(jobId); if (!result.success) { throw new Error(result.error); } if (result.data.success === false) { throw new Error(result.data.message || _("Service action failed")); } await waitForTachyonRunningState(expectedRunning); } catch (e) { logger.error("[DIAGNOSTIC]", `handleServiceRuntimeAction(${action})`, e); } finally { if (!delegatedToWatcher) { if (ownsJobFollow) { followedServiceActionJobs.delete(jobId); } setDiagnosticActionLoading(action, false); await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); if (jobId) { handledServiceActionJobs.add(jobId); void TachyonShellMethods.uiActionAck("service", jobId); } resetDiagnosticsChecks(); } } } function handleCheckServicesAction() { renderServiceCheckModal(); } async function handleRestart() { await handleServiceRuntimeAction({ action: "restart", expectedRunning: true, optimisticRunning: false }); } async function handleStart() { await handleServiceRuntimeAction({ action: "start", expectedRunning: true }); } async function handleStop() { await handleServiceRuntimeAction({ action: "stop", expectedRunning: false }); } async function handleEnable() { setDiagnosticActionLoading("enable", true); try { await TachyonShellMethods.enable(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleEnable - e", e); } finally { await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); setDiagnosticActionLoading("enable", false); } } async function handleDisable() { setDiagnosticActionLoading("disable", true); try { await TachyonShellMethods.disable(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleDisable - e", e); } finally { await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); setDiagnosticActionLoading("disable", false); } } async function handleShowGlobalCheck() { setDiagnosticActionLoading("globalCheck", true); try { const globalCheck = await TachyonShellMethods.globalCheck(false); if (globalCheck.success) { const rawGlobalCheckText = globalCheck.data ?? ""; const maskedGlobalCheckText = maskGlobalCheckText(rawGlobalCheckText); ui.showModal( _("Global check"), renderModal(rawGlobalCheckText, "global_check", { maskText: () => maskedGlobalCheckText, initialAutoRefresh: false, showMaskValuesToggle: true }) ); } else { logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", e); } finally { setDiagnosticActionLoading("globalCheck", false); } } async function handleRunDoctor() { setDiagnosticActionLoading("doctor", true); try { const doctorRes = await TachyonShellMethods.doctor(); if (!doctorRes || typeof doctorRes !== "object") { showToast(_("Doctor failed") + ": " + _("Unknown error"), "error"); return; } if (doctorRes.success) { const rawData = doctorRes.data; const data = typeof rawData === "object" && rawData !== null ? rawData : null; const report = data ? String(data.report ?? "") : String(rawData ?? ""); const issues = data ? Number(data.issues ?? 0) : 0; const fixed = data ? Number(data.fixed ?? 0) : 0; const title = issues > 0 ? _("Doctor repair") + " \u2014 " + _("Issues") + ": " + issues + ", " + _("Fixed") + ": " + fixed : /safe bypass|аварийного обхода|bypassed|стоковом состоянии|stock (state|internet)/i.test( report ) ? _("Doctor repair") + " \u2014 Safe Bypass" : _("Doctor repair") + " \u2014 " + _("No issues found"); ui.showModal( title, renderModal(report, "doctor_repair", { initialAutoRefresh: false }) ); } else { const errorMsg = typeof doctorRes.error === "string" ? doctorRes.error : _("Unknown error"); showToast(_("Doctor failed") + ": " + errorMsg, "error"); } } catch (e) { logger.error( "[DIAGNOSTIC]", "handleRunDoctor - e", e instanceof Error ? e.message : String(e) ); showToast(_("Doctor failed"), "error"); } finally { setDiagnosticActionLoading("doctor", false); runChecks(); } } function getAiDoctorHistory() { try { const raw = localStorage.getItem("tachyon_ai_doctor_history"); return raw ? JSON.parse(raw) : []; } catch (_e) { return []; } } function saveAiDoctorHistory(entry) { try { const current = getAiDoctorHistory(); const updated = [entry, ...current].slice(0, 5); localStorage.setItem("tachyon_ai_doctor_history", JSON.stringify(updated)); } catch (_e) { } } async function handleViewLogs() { setDiagnosticActionLoading("viewLogs", true); try { const viewLogs = await TachyonShellMethods.checkLogs(); if (viewLogs.success) { const getLatestLogs = async () => { const latestLogs = await TachyonShellMethods.checkLogs(); if (!latestLogs.success) { throw latestLogs; } return latestLogs.data ?? ""; }; ui.showModal( _("View logs"), renderModal(viewLogs.data, "view_logs", { getText: getLatestLogs, refreshMs: 250, initialAutoRefresh: true, showAutoRefreshToggle: true, startAtEnd: true }) ); } else { logger.error("[DIAGNOSTIC]", "handleViewLogs - e", viewLogs); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleViewLogs - e", e); } finally { setDiagnosticActionLoading("viewLogs", false); } } async function handleRunAiDoctor() { setDiagnosticActionLoading("aiDoctor", true); try { const aiRes = await TachyonShellMethods.aiDoctor(); if (!aiRes || typeof aiRes !== "object") { showToast(_("AI Doctor failed") + ": " + _("Unknown error"), "error"); return; } const rawData = aiRes.data; const data = typeof rawData === "object" && rawData !== null ? rawData : null; if (aiRes.success || data && data.success !== false) { const report = data ? String(data.report ?? data.summary ?? rawData ?? "") : String(rawData ?? ""); const quickFixes = Array.isArray(data?.quick_fixes) ? data.quick_fixes : String(data?.quick_fix ?? "").split(",").map((s) => s.trim()).filter(Boolean); const nowStr = (/* @__PURE__ */ new Date()).toLocaleTimeString(); saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes }); let historyEntries = getAiDoctorHistory(); let activeTab2 = "diagnosis"; let lanClients = []; let loadingClients = false; const loadLanClients = async () => { if (loadingClients) return; loadingClients = true; renderModalLayout(); try { const res = await TachyonShellMethods.getLanClients(); if (res && res.success && res.data && Array.isArray(res.data.clients)) { lanClients = res.data.clients; } } catch (e) { logger.error( "[DIAGNOSTIC]", "getLanClients error", e instanceof Error ? e.message : String(e) ); } finally { loadingClients = false; renderModalLayout(); } }; const copySupportReport = async () => { let currentClients = lanClients; if (currentClients.length === 0) { try { const res = await TachyonShellMethods.getLanClients(); if (res && res.success && res.data?.clients) { lanClients = res.data.clients; currentClients = lanClients; } } catch (_e) { } } const nodeSummary = nodes.map((n) => `${n.name}: ${n.status}`).join(" | "); const fixesSummary = quickFixes.length > 0 ? quickFixes.map((f) => FIX_LABELS[f] || f).join(", ") : _("None"); const clientsSummary = currentClients.length > 0 ? currentClients.map( (c) => `- ${c.hostname} (IP: ${c.ip}, MAC: ${c.mac.slice(0, 8)}**): ${c.mode.toUpperCase()}` ).join("\n") : _("No DHCP clients detected"); const text = [ "# Tachyon AI Doctor Diagnostic Report", `Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`, `Pillars: ${nodeSummary}`, `Recommended Fixes: ${fixesSummary}`, "", "## Diagnosis:", report, "", "## LAN Devices Routing:", clientsSummary ].join("\n"); try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } showToast( _("Anonymized support report copied to clipboard"), "success" ); } catch { showToast(_("Failed to copy report to clipboard"), "error"); } }; const getDeviceIcon = (hostname) => { const h = hostname.toLowerCase(); if (h.includes("tv") || h.includes("samsung") || h.includes("lg") || h.includes("bravia") || h.includes("roku") || h.includes("appletv")) return "\u{1F4FA}"; if (h.includes("phone") || h.includes("iphone") || h.includes("android") || h.includes("pixel") || h.includes("xiaomi") || h.includes("galaxy")) return "\u{1F4F1}"; if (h.includes("mac") || h.includes("pc") || h.includes("laptop") || h.includes("desktop") || h.includes("thinkpad")) return "\u{1F4BB}"; if (h.includes("playstation") || h.includes("ps4") || h.includes("ps5") || h.includes("xbox") || h.includes("switch") || h.includes("nintendo")) return "\u{1F3AE}"; return "\u{1F4DF}"; }; const repLower = report.toLowerCase(); const backendNodes = Array.isArray(data?.nodes) && data.nodes.length === 4 ? data.nodes : null; const nodes = backendNodes ?? [ { name: "WAN", status: repLower.includes("wan interface down") || repLower.includes( "\u0448\u043B\u044E\u0437 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0438\u043B\u0438 \u0432\u043D\u0435\u0448\u043D\u0438\u0439 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" ) || repLower.includes("wan interface is unreachable") ? "FAIL" : "OK" }, { name: "DNS", status: repLower.includes("\u0441\u0431\u043E\u0439 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F dns") || repLower.includes("dns resolution failed") || repLower.includes("dns failed") || repLower.includes("dnsmasq failed") ? "FAIL" : "OK" }, { name: "sing-box", status: (repLower.includes("sing-box") || repLower.includes("proxy")) && (repLower.includes("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D") || repLower.includes("stopped") || repLower.includes("\u043D\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u043E\u043D\u0438\u0440\u0443\u0435\u0442") || repLower.includes("error") || repLower.includes("crash")) ? "FAIL" : "OK" }, { name: "nftables", status: (repLower.includes("nftables") || repLower.includes("\u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u0444\u0430\u0439\u0440\u0432\u043E\u043B\u0430") || repLower.includes("firewall rules")) && (repLower.includes("\u043D\u0430\u0440\u0443\u0448\u0435\u043D\u044B") || repLower.includes("damaged") || repLower.includes("corrupted") || repLower.includes("compromised")) ? "WARN" : "OK" } ]; const FIX_LABELS = { start_singbox: _("Start sing-box"), rebuild_rules: _("Rebuild firewall rules"), fix_dnsmasq: _("Restart dnsmasq"), fix_resolv_symlink: _("Fix resolv.conf"), start_watchdog: _("Start watchdog"), restart_singbox_dns: _("Restart sing-box DNS"), fix_uci_config: _("Restore config backup"), fix_wan_interface: _("Reconnect WAN"), fix_gateway: _("Resolve gateway"), clear_dns_cache: _("Clear DNS cache"), update_subscriptions: _("Update subscriptions"), reset_firewall: _("Restart firewall"), restart_network: _("Restart network"), restart_zapret: _("Restart Zapret/ByeDPI"), optimize_memory: _("Optimize RAM memory"), switch_to_doh: _("Switch DNS to DoH"), heal_network_stack: _("Auto-Heal Network Stack"), enable_safe_bypass: _("Enable Direct WAN Bypass"), restore_native_internet: _("Restore Native Internet (Stop Tachyon)"), fix_system_time: _("Sync System Time (NTP)"), flush_conntrack: _("Flush Conntrack Table"), fix_bootstrap_dns: _("Reset Bootstrap DNS"), optimize_mtu: _("Optimize AWG MTU") }; const renderRootCauseBanner = () => { return E( "div", { class: "cbi-section-node", style: "display: flex; align-items: center; justify-content: space-around; gap: 8px; flex-wrap: wrap; padding: 8px 12px; margin-bottom: 12px; border-radius: 6px; background: var(--background-color-high, rgba(0,0,0,0.03)); border: 1px solid var(--border-color, rgba(0,0,0,0.1));" }, nodes.map((node) => { const labelClass = node.status === "OK" ? "label-success" : node.status === "WARN" ? "label-warning" : "label-danger"; const icon = node.status === "OK" ? "\u2713" : node.status === "WARN" ? "\u26A0" : "\u2715"; return E( "span", { class: `label ${labelClass}`, style: "font-size: 11px; padding: 4px 10px; border-radius: 4px; display: inline-flex; align-items: center; gap: 4px; font-weight: bold;" }, [ E("span", {}, node.name), E("span", {}, `${icon} ${node.status}`) ] ); }) ); }; const renderDiagnosisTabContent = () => { return E("div", { class: "cbi-section-node" }, [ renderRootCauseBanner(), E( "pre", { class: "tachyon-partial-modal__content alert-message notice", style: "white-space: pre-wrap; font-family: inherit; font-size: 12px; line-height: 1.5; max-height: 320px; overflow-y: auto; margin: 0; padding: 12px; border-radius: 6px; border: 1px solid var(--border-color, rgba(0,0,0,0.1));" }, report ), quickFixes.length > 0 ? E( "div", { class: "alert-message warning", style: "margin-top: 12px; padding: 10px 12px; border-radius: 6px;" }, [ E( "div", { style: "font-weight: bold; margin-bottom: 8px; font-size: 12px;" }, "\u{1F6E0}\uFE0F " + _("Recommended Quick Fixes:") ), E( "div", { style: "display: flex; gap: 6px; flex-wrap: wrap;" }, quickFixes.map((code) => { let applied = false; const friendlyLabel = FIX_LABELS[code] || code; const btn = renderButton({ classNames: ["cbi-button-apply"], text: `\u26A1 ${friendlyLabel}`, onClick: async () => { if (applied) return; btn.textContent = "\u23F3 " + _("Applying...") + " " + friendlyLabel; showToast( _("Applying fix") + ": " + friendlyLabel + "...", "success" ); const fixRes = await TachyonShellMethods.applyQuickFix(code); if (fixRes && typeof fixRes === "object" && fixRes.success) { applied = true; btn.textContent = `\u2713 ${friendlyLabel} (${_("Fixed")})`; btn.classList.remove("cbi-button-apply"); btn.classList.add("cbi-button-neutral"); showToast( _("Fix applied") + ": " + friendlyLabel, "success" ); } else { btn.textContent = `\u26A1 ${friendlyLabel}`; showToast( _("Failed to apply fix") + ": " + friendlyLabel, "error" ); } } }); return btn; }) ) ] ) : nodes.some((n) => n.status === "FAIL" || n.status === "WARN") ? E( "div", { class: "alert-message warning", style: "margin-top: 12px; padding: 8px 12px; font-size: 12px; border-radius: 6px;" }, "\u26A0\uFE0F " + _("Issues detected. Review the diagnosis above.") ) : E( "div", { class: "alert-message success", style: "margin-top: 12px; padding: 8px 12px; font-size: 12px; border-radius: 6px;" }, "\u2713 " + _("No issues detected. System is running normally.") ) ]); }; const renderDevicesTabContent = () => { if (loadingClients) { return E( "div", { class: "cbi-section-node", style: "padding: 20px; text-align: center; font-size: 13px;" }, "\u23F3 " + _("Loading connected LAN devices...") ); } if (lanClients.length === 0) { return E("div", { class: "cbi-section-node" }, [ E( "div", { class: "alert-message info", style: "margin: 0 0 10px 0; padding: 12px;" }, _("No active DHCP clients found on local network.") ), renderButton({ classNames: ["cbi-button-action"], text: "\u{1F504} " + _("Refresh Device List"), onClick: loadLanClients }) ]); } return E("div", { class: "cbi-section-node" }, [ E( "div", { style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;" }, [ E( "span", { style: "font-weight: bold; font-size: 12px;" }, `\u{1F4F1} ${_("Connected Devices")}: ${lanClients.length}` ), renderButton({ classNames: ["cbi-button-neutral"], text: "\u{1F504} " + _("Refresh"), onClick: loadLanClients }) ] ), E( "div", { style: "display: flex; flex-direction: column; gap: 8px; max-height: 340px; overflow-y: auto;" }, lanClients.map((client) => { const icon = getDeviceIcon(client.hostname); const isDirect = client.mode === "direct"; return E( "div", { class: "cbi-section-node", style: "display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-color, rgba(0,0,0,0.1)); background: var(--background-color-high, rgba(0,0,0,0.02)); flex-wrap: wrap;" }, [ E( "div", { style: "display: flex; align-items: center; gap: 8px;" }, [ E("span", { style: "font-size: 18px;" }, icon), E("div", {}, [ E( "div", { style: "font-weight: bold; font-size: 12px;" }, client.hostname ), E( "div", { style: "font-size: 11px; opacity: 0.75;" }, `${client.ip} (${client.mac})` ) ]) ] ), E( "div", { style: "display: flex; align-items: center; gap: 8px;" }, [ E( "span", { class: `label ${isDirect ? "label-warning" : "label-success"}`, style: "font-size: 11px; padding: 3px 8px; border-radius: 4px;" }, isDirect ? "\u{1F310} " + _("Direct WAN") : "\u{1F6E1}\uFE0F " + _("Proxy / DPI") ), renderButton({ classNames: [ isDirect ? "cbi-button-action" : "cbi-button-apply" ], text: isDirect ? "\u{1F6E1}\uFE0F " + _("Route via Proxy") : "\u26A1 " + _("Direct Bypass"), onClick: async () => { showToast( _("Updating device routing mode..."), "success" ); const res = await TachyonShellMethods.toggleClientBypass( client.ip ); if (res && res.success && res.data) { client.mode = res.data.mode; showToast( _("Device updated") + ": " + client.hostname, "success" ); renderModalLayout(); } else { showToast(_("Failed to update device"), "error"); } } }) ] ) ] ); }) ) ]); }; const renderHistoryTabContent = () => { if (historyEntries.length === 0) { return E( "div", { class: "alert-message info", style: "margin: 0; padding: 12px;" }, _("No diagnostic history available yet.") ); } return E( "div", { style: "display: flex; flex-direction: column; gap: 8px; max-height: 360px; overflow-y: auto;" }, historyEntries.map( (h, i) => E( "div", { class: "cbi-section-node", style: "padding: 10px; border-radius: 6px; border: 1px solid var(--border-color, rgba(0,0,0,0.1)); background: var(--background-color-high, rgba(0,0,0,0.02));" }, [ E( "div", { style: "display: flex; justify-content: space-between; font-weight: bold; font-size: 11px; margin-bottom: 6px; opacity: 0.8;" }, [ E("span", {}, `#${historyEntries.length - i}`), E("span", {}, h.timestamp) ] ), E( "pre", { class: "tachyon-partial-modal__content", style: "margin: 0; white-space: pre-wrap; font-size: 11px; max-height: 120px; overflow-y: auto; padding: 8px; border-radius: 4px; background: rgba(0,0,0,0.05);" }, h.report ) ] ) ) ); }; const mainContainer = E( "div", { class: "tachyon-partial-modal__body", style: "width: 100%; max-width: 680px; box-sizing: border-box;" }, [] ); const renderModalLayout = () => { mainContainer.replaceChildren( E("div", {}, [ E( "div", { style: "display: flex; gap: 8px; margin-bottom: 12px; border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1)); padding-bottom: 8px; flex-wrap: wrap;" }, [ renderButton({ classNames: [ activeTab2 === "diagnosis" ? "cbi-button-action" : "cbi-button-neutral" ], text: "\u{1F50D} " + _("Current Diagnosis"), onClick: () => { activeTab2 = "diagnosis"; renderModalLayout(); } }), renderButton({ classNames: [ activeTab2 === "devices" ? "cbi-button-action" : "cbi-button-neutral" ], text: `\u{1F4F1} ${_("LAN Devices")} ${lanClients.length > 0 ? `(${lanClients.length})` : ""}`, onClick: () => { activeTab2 = "devices"; if (lanClients.length === 0) { loadLanClients(); } else { renderModalLayout(); } } }), renderButton({ classNames: [ activeTab2 === "history" ? "cbi-button-action" : "cbi-button-neutral" ], text: `\u{1F552} ${_("History")} (${historyEntries.length})`, onClick: () => { historyEntries = getAiDoctorHistory(); activeTab2 = "history"; renderModalLayout(); } }) ] ), activeTab2 === "diagnosis" ? renderDiagnosisTabContent() : activeTab2 === "devices" ? renderDevicesTabContent() : renderHistoryTabContent(), E( "div", { class: "tachyon-partial-modal__footer", style: "margin-top: 15px; display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap;" }, [ renderButton({ classNames: ["cbi-button-action"], text: "\u{1F4CB} " + _("Copy Support Report"), onClick: copySupportReport }), renderButton({ classNames: ["cbi-button-neutral"], text: _("Close"), onClick: () => ui.hideModal() }) ] ) ]) ); }; renderModalLayout(); ui.showModal(_("AI Doctor Diagnosis"), mainContainer); } else { const errorMsg = typeof aiRes.error === "string" ? aiRes.error : _("Unknown error"); showToast(_("AI Doctor failed") + ": " + errorMsg, "error"); } } catch (e) { logger.error( "[DIAGNOSTIC]", "handleRunAiDoctor - e", e instanceof Error ? e.message : String(e) ); showToast(_("AI Doctor failed"), "error"); } finally { setDiagnosticActionLoading("aiDoctor", false); } } function handleOpenAiChat() { renderAiChatModal(); } async function handleShowSingBoxConfig() { setDiagnosticActionLoading("showSingBoxConfig", true); try { const showSingBoxConfig = await TachyonShellMethods.showSingBoxConfig(false); if (showSingBoxConfig.success) { const rawSingBoxConfigText = stringifySingBoxConfig( showSingBoxConfig.data ); const maskedSingBoxConfigText = formatMaskedSingBoxConfig( showSingBoxConfig.data ); ui.showModal( _("Show sing-box config"), renderModal(rawSingBoxConfigText, "show_sing_box_config", { maskText: () => maskedSingBoxConfigText, initialAutoRefresh: false, showMaskValuesToggle: true }) ); } else { logger.error( "[DIAGNOSTIC]", "handleShowSingBoxConfig - e", showSingBoxConfig ); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleShowSingBoxConfig - e", e); } finally { setDiagnosticActionLoading("showSingBoxConfig", false); } } async function handleGenerateBugReport() { setDiagnosticActionLoading("generateBugReport", true); try { const configResult = await fs.read("/etc/config/tachyon").catch(() => ""); const logsResult = await executeShellCommand({ command: "/sbin/logread", args: ["-e", "tachyon", "-l", "1000"] }); const singboxLogsResult = await executeShellCommand({ command: "/sbin/logread", args: ["-e", "sing-box", "-l", "1000"] }); const rawReport = [ "--- TACHYON CONFIG ---", configResult || "Failed to fetch config", "", "--- TACHYON LOGS ---", logsResult.code === 0 ? logsResult.stdout : "Failed to fetch tachyon logs", "", "--- SING-BOX LOGS ---", singboxLogsResult.code === 0 ? singboxLogsResult.stdout : "Failed to fetch sing-box logs" ].join("\n"); const maskedReport = maskGlobalCheckText(rawReport); const blob = new Blob([maskedReport], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `tachyon-bugreport-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.txt`; a.style.display = "none"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(_("Bug report downloaded"), "success"); } catch (e) { logger.error("[DIAGNOSTIC]", "handleGenerateBugReport - e", e); showToast(_("Failed to generate bug report"), "error"); } finally { setDiagnosticActionLoading("generateBugReport", false); } } function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; function getWikiKind() { const allResults = diagnosticsChecks.map((check) => check.state); if (allResults.includes("error")) { return "error"; } if (allResults.includes("warning")) { return "warning"; } return "default"; } const container = document.getElementById("tachyon_diagnostic-page-wiki"); return preserveScrollForPage(() => { container.replaceChildren(renderWikiDisclaimer(getWikiKind())); }); } function renderDiagnosticAvailableActionsWidget() { const diagnosticsActions = store.get().diagnosticsActions; const updatesActions = store.get().updatesActions; const servicesInfoWidget = store.get().servicesInfoWidget; logger.debug("[DIAGNOSTIC]", "renderDiagnosticAvailableActionsWidget"); const tachyonEnabled = Boolean(servicesInfoWidget.data.tachyonEnabled); const tachyonRunning = Boolean(servicesInfoWidget.data.tachyonRunning); const serviceTransition = getServiceTransition( servicesInfoWidget.data.tachyonStatus ); const restartLoading = diagnosticsActions.restart.loading || serviceTransition.restarting; const startLoading = diagnosticsActions.start.loading || serviceTransition.starting; const stopLoading = diagnosticsActions.stop.loading || serviceTransition.stopping; const atLeastOneMutatingActionLoading = restartLoading || startLoading || stopLoading || diagnosticsActions.enable.loading || diagnosticsActions.disable.loading; const componentActionLoading = hasComponentActionLoading(updatesActions); const { serviceControlsDisabled, utilityActionsDisabled, viewLogsDisabled } = getAvailableActionsDisabledState({ servicesInfoLoading: servicesInfoWidget.loading, mutatingServiceActionLoading: atLeastOneMutatingActionLoading, componentActionLoading }); const startVisible = shouldShowStartAction({ tachyonRunning, restartLoading, startLoading, stopLoading }); const stopVisible = shouldShowStopAction({ tachyonRunning, restartLoading, startLoading, stopLoading }); const container = document.getElementById("tachyon_diagnostic-page-actions"); const renderedActions = renderAvailableActions({ restart: { loading: restartLoading, visible: shouldShowRestartAction({ tachyonRunning, restartLoading, startLoading, stopLoading }), onClick: handleRestart, disabled: serviceControlsDisabled }, start: { loading: startLoading, visible: startVisible, onClick: handleStart, disabled: serviceControlsDisabled }, stop: { loading: stopLoading, visible: stopVisible, onClick: handleStop, disabled: serviceControlsDisabled }, enable: { loading: diagnosticsActions.enable.loading, visible: !tachyonEnabled, onClick: handleEnable, disabled: serviceControlsDisabled }, disable: { loading: diagnosticsActions.disable.loading, visible: tachyonEnabled, onClick: handleDisable, disabled: serviceControlsDisabled }, globalCheck: { loading: diagnosticsActions.globalCheck.loading, visible: true, onClick: handleShowGlobalCheck, disabled: utilityActionsDisabled }, doctor: { loading: diagnosticsActions.doctor.loading, visible: true, onClick: handleRunDoctor, disabled: utilityActionsDisabled }, aiDoctor: { loading: diagnosticsActions.aiDoctor.loading, visible: true, onClick: handleRunAiDoctor, disabled: diagnosticsActions.aiDoctor.loading }, aiChat: { loading: false, visible: true, onClick: handleOpenAiChat, disabled: false }, checkServices: { visible: true, loading: diagnosticsActions.checkServices.loading, disabled: utilityActionsDisabled, onClick: handleCheckServicesAction }, viewLogs: { loading: diagnosticsActions.viewLogs.loading, visible: true, onClick: handleViewLogs, disabled: viewLogsDisabled }, showSingBoxConfig: { loading: diagnosticsActions.showSingBoxConfig.loading, visible: true, onClick: handleShowSingBoxConfig, disabled: utilityActionsDisabled }, generateBugReport: { loading: diagnosticsActions.generateBugReport.loading, visible: true, onClick: handleGenerateBugReport, disabled: utilityActionsDisabled } }); return preserveScrollForPage(() => { container.replaceChildren(renderedActions); }); } function renderDiagnosticSystemInfoWidget() { logger.debug("[DIAGNOSTIC]", "renderDiagnosticSystemInfoWidget"); const diagnosticsSystemInfo = store.get().diagnosticsSystemInfo; const container = document.getElementById( "tachyon_diagnostic-page-system-info" ); const items = [ { key: "Tachyon", value: normalizeCompiledVersion( diagnosticsSystemInfo.tachyon_version, diagnosticsSystemInfo.tachyon_commit_sha ) }, { key: "Luci App", value: normalizeCompiledVersion(TACHYON_LUCI_APP_VERSION) }, { key: "Sing-box", value: formatSingBoxVersion(diagnosticsSystemInfo) } ]; if (diagnosticsSystemInfo.zapret_installed) { items.push({ key: "Zapret", value: diagnosticsSystemInfo.zapret_version }); } if (diagnosticsSystemInfo.zapret2_installed) { items.push({ key: "Zapret2", value: diagnosticsSystemInfo.zapret2_version }); } if (diagnosticsSystemInfo.byedpi_installed) { items.push({ key: "ByeDPI", value: diagnosticsSystemInfo.byedpi_version }); } items.push( { key: "OS", value: diagnosticsSystemInfo.openwrt_version }, { key: "Device", value: diagnosticsSystemInfo.device_model } ); const renderedSystemInfo = renderSystemInfo({ items }); return preserveScrollForPage(() => { container.replaceChildren(renderedSystemInfo); }); } async function onStoreUpdate2(_next, _prev, diff) { if (diff.diagnosticsChecks) { renderDiagnosticsChecks(); renderWikiDisclaimerWidget(); } if (diff.diagnosticsRunAction) { renderDiagnosticRunActionWidget(); } if (diff.diagnosticsActions || diff.servicesInfoWidget || diff.updatesActions) { renderDiagnosticAvailableActionsWidget(); } if (diff.diagnosticsActions || diff.servicesInfoWidget) { renderDiagnosticRunActionWidget(); } if (diff.diagnosticsSystemInfo) { renderDiagnosticSystemInfoWidget(); renderDiagnosticRunActionWidget(); } } function persistDiagnosticRunProgress({ providerOptions, nextRunnerIndex }) { savePersistedDiagnosticRun({ providerOptions, nextRunnerIndex, diagnosticsChecks: store.get().diagnosticsChecks }); } function setDiagnosticCheckLoading(code) { const meta = DIAGNOSTICS_CHECKS_MAP[code]; const diagnosticsChecks = store.get().diagnosticsChecks; const other = diagnosticsChecks.filter((item) => item.code !== code); store.set({ diagnosticsChecks: [ ...other, { order: meta.order, code: meta.code, title: meta.title, description: _("Checking, please wait"), state: "loading", items: [] } ] }); } function getDiagnosticRunners(providerOptions) { return [ { code: "DNS" /* DNS */, run: runDnsCheck }, { code: "SINGBOX" /* SINGBOX */, run: runSingBoxCheck }, ...providerOptions.includeInbounds ? [{ code: "INBOUNDS" /* INBOUNDS */, run: runInboundsCheck }] : [], { code: "NFT" /* NFT */, run: runNftCheck }, ...providerOptions.includeZapret ? [{ code: "ZAPRET" /* ZAPRET */, run: runZapretCheck }] : [], ...providerOptions.includeZapret2 ? [{ code: "ZAPRET2" /* ZAPRET2 */, run: runZapret2Check }] : [], ...providerOptions.includeByedpi ? [{ code: "BYEDPI" /* BYEDPI */, run: runByedpiCheck }] : [], { code: "OUTBOUNDS" /* OUTBOUNDS */, run: runSectionsCheck }, { code: "FAKEIP" /* FAKEIP */, run: runFakeIPCheck } ]; } async function runChecks({ resume } = {}) { if (store.get().diagnosticsRunAction.loading && !resume) { return; } let providerOptions = resume?.providerOptions ?? getDiagnosticsProviderOptions(); let nextRunnerIndex = resume?.nextRunnerIndex ?? 0; store.set({ diagnosticsRunAction: { loading: true }, diagnosticsChecks: resume?.diagnosticsChecks ?? getLoadingDiagnosticsChecks(providerOptions).diagnosticsChecks }); persistDiagnosticRunProgress({ providerOptions, nextRunnerIndex }); try { if (!resume) { await fetchDiagnosticsProviderInfo({ resetChecks: false }); providerOptions = getDiagnosticsProviderOptions(); nextRunnerIndex = 0; store.set({ diagnosticsChecks: getLoadingDiagnosticsChecks(providerOptions).diagnosticsChecks }); persistDiagnosticRunProgress({ providerOptions, nextRunnerIndex }); } const runners = getDiagnosticRunners(providerOptions); for (let index = nextRunnerIndex; index < runners.length; index += 1) { const runner = runners[index]; setDiagnosticCheckLoading(runner.code); persistDiagnosticRunProgress({ providerOptions, nextRunnerIndex: index }); try { await runner.run(); } catch (e) { logger.error( "[DIAGNOSTIC]", `runChecks - ${runner.run.name} failed`, e ); } persistDiagnosticRunProgress({ providerOptions, nextRunnerIndex: index + 1 }); } } catch (e) { logger.error("[DIAGNOSTIC]", "runChecks - e", e); } finally { clearPersistedDiagnosticRun(); store.set({ diagnosticsRunAction: { loading: false } }); if (!diagnosticMounted) { diagnosticCompletedWhileHidden = true; } } } async function loadInitialDiagnosticData() { const diagnosticStatus = document.getElementById("diagnostic-status"); if (diagnosticStatus?.isConnected && diagnosticStatus.offsetParent !== null) { if (store.get().diagnosticsRunAction.loading) { return; } await fetchSystemInfo(); await fetchDiagnosticsProviderInfo(); } } function restorePersistedDiagnosticRun() { const persistedRun = readPersistedDiagnosticRun(); if (!persistedRun) { return false; } store.set({ diagnosticsRunAction: { loading: true }, diagnosticsChecks: persistedRun.diagnosticsChecks }); void runChecks({ resume: persistedRun }); return true; } async function onPageMount2() { const preserveHiddenResult = diagnosticCompletedWhileHidden; onPageUnmount2({ preserveCompletedResult: preserveHiddenResult, preservePersistedRun: true }); diagnosticMounted = true; diagnosticMountId += 1; const mountId = diagnosticMountId; const hasRuntimeSnapshot = Boolean(getCachedRuntimeUiState()); if (!hasRuntimeSnapshot) { const uiState = await refreshRuntimeUiState({ force: true }); if (!diagnosticMounted || mountId !== diagnosticMountId) { return; } if (!uiState) { void refreshDiagnosticServicesInfo({ force: true }); } } const restoredPersistedRun = !preserveHiddenResult && restorePersistedDiagnosticRun(); if (preserveHiddenResult) { diagnosticCompletedWhileHidden = false; } else if (!restoredPersistedRun && !store.get().diagnosticsRunAction.loading) { store.reset(["diagnosticsRunAction"]); resetDiagnosticsChecks(); } store.subscribe(onStoreUpdate2); startServiceActionStateWatcher(); renderDiagnosticsChecks(); renderDiagnosticRunActionWidget(); renderDiagnosticAvailableActionsWidget(); renderDiagnosticSystemInfoWidget(); renderWikiDisclaimerWidget(); if (hasRuntimeSnapshot) { void refreshRuntimeUiState({ force: true }); } if (!preserveHiddenResult && !restoredPersistedRun) { void loadInitialDiagnosticData(); } } function onPageUnmount2({ preserveCompletedResult = false, preservePersistedRun = false } = {}) { diagnosticMounted = false; diagnosticMountId += 1; stopServiceActionStateWatcher(); servicesInfoRefreshPromise = null; store.unsubscribe(onStoreUpdate2); if (!preserveCompletedResult && !store.get().diagnosticsRunAction.loading) { if (!preservePersistedRun) { clearPersistedDiagnosticRun(); } store.reset(["diagnosticsRunAction"]); resetDiagnosticsChecks(); diagnosticCompletedWhileHidden = false; } } function registerLifecycleListeners2() { if (diagnosticLifecycleRegistered) { return; } diagnosticLifecycleRegistered = true; store.subscribe((next, prev, diff) => { if (diff.tabService && next.tabService.current !== prev.tabService.current) { logger.debug( "[DIAGNOSTIC]", "active tab diff event, active tab:", diff.tabService.current ); const isDIAGNOSTICVisible = next.tabService.current === "diagnostic"; if (isDIAGNOSTICVisible) { logger.debug( "[DIAGNOSTIC]", "registerLifecycleListeners", "onPageMount" ); return onPageMount2(); } if (!isDIAGNOSTICVisible) { logger.debug( "[DIAGNOSTIC]", "registerLifecycleListeners", "onPageUnmount" ); return onPageUnmount2(); } } }); } async function initController2() { if (diagnosticControllerInitialized) { return; } diagnosticControllerInitialized = true; onMount("diagnostic-status").then(() => { logger.debug("[DIAGNOSTIC]", "initController", "onMount"); registerLifecycleListeners2(); if (store.get().tabService.current === "diagnostic" || isActiveLuciTab("diagnostic")) { onPageMount2(); } }); } // src/tachyon/tabs/diagnostic/styles.ts var styles4 = ` #cbi-${TACHYON_UCI_PACKAGE}-diagnostic-_mount_node > div { width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-diagnostic > h3 { display: none; } .tachyon_diagnostic-page { display: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 10px; align-items: start; } @media (max-width: 800px) { .tachyon_diagnostic-page { grid-template-columns: 1fr; } } .tachyon_diagnostic-page__right-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .tachyon_diagnostic-page__right-bar__wiki { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; display: grid; grid-template-columns: auto; grid-row-gap: 10px; } .tachyon_diagnostic-page__right-bar__wiki--warning { border: 2px var(--warn-color-medium, orange) solid; } .tachyon_diagnostic-page__right-bar__wiki--error { border: 2px var(--error-color-medium, red) solid; } .tachyon_diagnostic-page__right-bar__wiki__content { display: grid; grid-template-columns: 1fr 5fr; grid-column-gap: 10px; } .tachyon_diagnostic-page__right-bar__wiki__texts {} .tachyon_diagnostic-page__right-bar__actions { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; display: grid; grid-template-columns: auto; grid-row-gap: 10px; } .tachyon_diagnostic-page__right-bar__actions > .tachyon-partial-button { width: 100%; min-width: 0; margin-left: 0; } .tachyon_diagnostic-page__right-bar__system-info { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; display: grid; grid-template-columns: auto; grid-row-gap: 10px; } .tachyon_diagnostic-page__right-bar__system-info__title { } .tachyon_diagnostic-page__right-bar__system-info__row { display: grid; grid-template-columns: auto 1fr; grid-column-gap: 5px; } .tachyon_diagnostic-page__right-bar__system-info__row__tag { padding: 2px 4px; border: 1px transparent solid; border-radius: 4px; margin-left: 5px; } .tachyon_diagnostic-page__right-bar__system-info__row__tag--neutral { border: 1px var(--background-color-high, gray) solid; color: var(--text-color-medium, gray); } .tachyon_diagnostic-page__right-bar__system-info__row__tag--warning { border: 1px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .tachyon_diagnostic-page__right-bar__system-info__row__tag--success { border: 1px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .tachyon_diagnostic-page__left-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .tachyon_diagnostic-page__run_check_wrapper {} .tachyon_diagnostic-page__run_check_wrapper button { width: 100%; } .tachyon_diagnostic-page__checks { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .tachyon_diagnostic_alert { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; display: grid; grid-template-columns: 24px 1fr; grid-column-gap: 10px; align-items: center; padding: 10px; } .tachyon_diagnostic_alert--loading { border: 2px var(--primary-color-high, dodgerblue) solid; } .tachyon_diagnostic_alert--warning { border: 2px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .tachyon_diagnostic_alert--error { border: 2px var(--error-color-medium, red) solid; color: var(--error-color-medium, red); } .tachyon_diagnostic_alert--success { border: 2px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .tachyon_diagnostic_alert--skipped {} .tachyon_diagnostic_alert__icon {} .tachyon_diagnostic_alert__content {} .tachyon_diagnostic_alert__title { display: block; } .tachyon_diagnostic_alert__description {} .tachyon_diagnostic_alert__summary { margin-top: 10px; } .tachyon_diagnostic_alert__summary__item { display: grid; grid-template-columns: 16px auto 1fr; grid-column-gap: 10px; } .tachyon_diagnostic_alert__summary__item--error { color: var(--error-color-medium, red); } .tachyon_diagnostic_alert__summary__item--warning { color: var(--warn-color-medium, orange); } .tachyon_diagnostic_alert__summary__item--success { color: var(--success-color-medium, green); } .tachyon_diagnostic_alert__summary__item__icon { width: 16px; height: 16px; } `; // src/tachyon/tabs/diagnostic/index.ts var DiagnosticTab = { render: render2, initController: initController2, styles: styles4 }; // src/tachyon/tabs/monitoring/render.ts function render3() { return E( "div", { id: "monitoring-status", class: "tachyon_monitoring-page" }, [ E("div", { class: "tachyon_monitoring-page__panel" }, [ E("div", { class: "tachyon_monitoring-page__controls" }, [ E("div", { class: "tachyon_monitoring-page__tabs" }, [ E( "button", { id: "monitoring-tab-active", class: "btn cbi-button tachyon_monitoring-page__tab tachyon_monitoring-page__tab--active", type: "button" }, `${_("Active")} 0` ), E( "button", { id: "monitoring-tab-closed", class: "btn cbi-button tachyon_monitoring-page__tab", type: "button" }, `${_("Closed")} 0` ) ]), E("div", { class: "tachyon_monitoring-page__filters" }, [ E( "select", { id: "monitoring-device-filter", class: "cbi-input-select tachyon_monitoring-page__device-filter" }, [E("option", { value: "all" }, _("All"))] ), E( "select", { id: "monitoring-route-filter", class: "cbi-input-select tachyon_monitoring-page__device-filter tachyon_monitoring-page__route-filter" }, [E("option", { value: "all" }, _("All Routes"))] ), E("label", { class: "tachyon_monitoring-page__search" }, [ E("span", { class: "tachyon_monitoring-page__search-icon" }, []), E("input", { id: "monitoring-search", class: "cbi-input-text tachyon_monitoring-page__search-input", type: "search", placeholder: _("Search"), autocomplete: "off" }) ]) ]), E("div", { class: "tachyon_monitoring-page__actions" }, [ E( "button", { id: "monitoring-close-all", class: "btn cbi-button tachyon_monitoring-page__icon-button", title: _("Close all connections"), "aria-label": _("Close all connections"), type: "button", disabled: true }, [] ), E( "button", { id: "monitoring-pause-toggle", class: "btn cbi-button tachyon_monitoring-page__icon-button", title: _("Pause updates"), "aria-label": _("Pause updates"), type: "button" }, [] ) ]) ]), E( "div", { id: "monitoring-connections", class: "tachyon_monitoring-page__body" }, [ E( "div", { class: "tachyon_monitoring-page__state tachyon_monitoring-page__state--loading" }, _("Loading connections") ) ] ) ]) ] ); } // src/tachyon/tabs/monitoring/initController.ts function normalizeConnectionsPayload(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return {}; } return value; } var RENDER_INTERVAL_MS = 500; var CONNECTIONS_RPC_POLL_INTERVAL_MS = 1500; var CLOSED_CONNECTION_LIMIT = 300; var ALL_FILTER_VALUE = "all"; var dependencies = {}; var monitoringMounted = false; var monitoringMountId = 0; var monitoringLifecycleRegistered = false; var monitoringControllerInitialized = false; var serviceStateUnsubscribe = null; var renderTimer = null; var connectionsPollTimer = null; var connectionsSocketUrl = ""; var connectionsUpdatesId = 0; var renderSkippedForSelection = false; var pendingConnectionsPayload = null; var pollingConnections = false; var activeTab = "active"; var selectedDeviceFilter = ALL_FILTER_VALUE; var selectedRouteFilter = ALL_FILTER_VALUE; var searchQuery = ""; var localDeviceChoices = {}; var routeDisplayNames = {}; var routeSections = []; var serverDisplayNames = {}; var lastDeviceFilterSignature = ""; var lastRouteFilterSignature = ""; var loading = true; var failed = false; var closingAll = false; var monitoringPaused = false; var monitoringPausedAt = null; var serviceAvailability = "loading"; var activeConnections = /* @__PURE__ */ new Map(); var closedConnections = /* @__PURE__ */ new Map(); var closingConnectionIds = /* @__PURE__ */ new Set(); function normalizeString(value) { return value == null ? "" : String(value).trim(); } function getListValues2(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map((item) => normalizeString(item)).filter(Boolean); } return normalizeString(value).split(/\s+/).map((item) => item.trim()).filter(Boolean); } function getUrlTestIds2(section) { const values = getListValues2(section.urltests); return values.length ? values : section.urltest_enabled === "1" ? ["urltest"] : []; } function getUrlTestTag2(sectionName, id) { return getOutboundTagBySection( id === "urltest" ? `${sectionName}-urltest` : `${sectionName}-urltest-${id}` ); } function formatEndpoint(address, port) { const normalizedAddress = normalizeString(address); const normalizedPort = normalizeString(port); if (!normalizedAddress) { return "-"; } if (!normalizedPort) { return normalizedAddress; } if (normalizedPort === "443") { return normalizedAddress; } if (normalizedAddress.includes(":") && !normalizedAddress.startsWith("[")) { return `[${normalizedAddress}]:${normalizedPort}`; } return `${normalizedAddress}:${normalizedPort}`; } function getDisplayName2(section) { return normalizeString(section.label) || section[".name"]; } function buildRouteDisplayNames(sections) { const map = { "bypass-out": "Bypass", "direct-out": "direct" }; const serverMap = {}; const routeSectionItems = []; const urltestsBySection = /* @__PURE__ */ new Map(); sections.filter((section) => section[".type"] === "urltest").forEach((section) => { const owner = normalizeString(section.section); const id = normalizeString(section.id) || section[".name"]; if (!owner || !id) { return; } urltestsBySection.set(owner, [ ...urltestsBySection.get(owner) || [], id ]); }); sections.filter((section) => section[".type"] === "section").filter((section) => section.enabled !== "0").forEach((section) => { const sectionName = section[".name"]; const displayName = getDisplayName2(section); if (!sectionName || !displayName) { return; } routeSectionItems.push({ sectionName, displayName }); map[getOutboundTagBySection(sectionName)] = displayName; const urltestIds = urltestsBySection.get(sectionName) || getUrlTestIds2(section); urltestIds.forEach((id) => { map[getUrlTestTag2(sectionName, id)] = displayName; }); }); sections.filter((section) => section[".type"] === "server").filter((section) => section.enabled !== "0").forEach((section) => { const sectionName = section[".name"]; const displayName = getDisplayName2(section); if (!sectionName || !displayName) { return; } serverMap[`server-${sectionName}-in`] = displayName; }); routeDisplayNames = map; serverDisplayNames = serverMap; routeSections = routeSectionItems.sort( (a, b) => b.sectionName.length - a.sectionName.length ); } function getRouteDisplayNameByTag(tag) { if (!tag) { return ""; } if (routeDisplayNames[tag]) { return routeDisplayNames[tag]; } const manualSection = routeSections.find(({ sectionName }) => { if (!tag.startsWith(`${sectionName}-`) || !tag.endsWith("-out")) { return false; } const middle = tag.slice(sectionName.length + 1, -4); return /^\d+(?:-\d+)?$/.test(middle); }); return manualSection?.displayName || ""; } function getRouteTagFromRule(rule) { const match = normalizeString(rule).match(/=>\s*route\(([^)]+)\)/); return normalizeString(match?.[1]).replace(/^['"]|['"]$/g, ""); } function parseStartedAt(connection) { const startedAt = Date.parse(connection.start || ""); return Number.isFinite(startedAt) ? startedAt : connection.lastSeenAt; } function formatDuration(ms) { const totalSeconds = Math.max(0, Math.floor(ms / 1e3)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor(totalSeconds % 3600 / 60); const seconds = totalSeconds % 60; const pad = (value) => String(value).padStart(2, "0"); if (hours > 0) { return `${hours}:${pad(minutes)}:${pad(seconds)}`; } return `${minutes}:${pad(seconds)}`; } function formatConnectionDuration(connection) { const startedAt = parseStartedAt(connection); const finishedAt = connection.closedAt || monitoringPausedAt || Date.now(); return formatDuration(finishedAt - startedAt); } function formatBytes2(value) { return prettyBytes(Number.isFinite(value) ? Number(value) : 0); } function getConnectionSourceIp(connection) { return normalizeString(connection.metadata?.sourceIP); } function getConnectionInboundTag(connection) { const metadataType = normalizeString(connection.metadata?.type); const metadataTypeParts = metadataType.split("/"); const metadataTag = normalizeString( metadataTypeParts.length > 1 ? metadataTypeParts[metadataTypeParts.length - 1] : metadataType ); if (metadataTag) { return metadataTag; } const ruleInbound = normalizeString(connection.rule).match( /(?:^|\s)inbound=([^\s]+)/ ); return normalizeString(ruleInbound?.[1]); } function getServerDisplayNameByInboundTag(tag) { return normalizeString(serverDisplayNames[tag]); } function getDeviceName(ip) { return normalizeString(localDeviceChoices[ip]); } function getServerSourceNameByIp(ip) { if (!ip) { return ""; } const connections = [ ...Array.from(activeConnections.values()), ...Array.from(closedConnections.values()) ]; for (const connection of connections) { if (getConnectionSourceIp(connection) !== ip) { continue; } const serverName = getServerDisplayNameByInboundTag( getConnectionInboundTag(connection) ); if (serverName) { return serverName; } } return ""; } function getDeviceFilterLabel(ip) { const serverName = getServerSourceNameByIp(ip); if (serverName) { return serverName; } const deviceName = getDeviceName(ip); return deviceName || ip; } function getSourceCellParts(connection) { const ip = getConnectionSourceIp(connection); const inboundTag = getConnectionInboundTag(connection); const serverName = getServerDisplayNameByInboundTag(inboundTag); if (serverName) { return { primary: serverName, ip: "", copyValue: serverName, searchValue: [serverName, ip, inboundTag].filter(Boolean).join(" ") }; } const deviceName = getDeviceName(ip); if (deviceName) { return { primary: deviceName, ip: "", copyValue: deviceName, searchValue: `${deviceName} ${ip}` }; } return { primary: ip || "-", ip: "", copyValue: ip || "-", searchValue: ip }; } function getTargetCellParts(connection) { const metadata = connection.metadata || {}; const host = normalizeString(metadata.host); const destinationIp = normalizeString(metadata.destinationIP); const port = metadata.destinationPort; const primaryTarget = host || destinationIp; const primary = primaryTarget ? formatEndpoint(primaryTarget, port) : "-"; return { primary, searchValue: [primary, host, destinationIp].filter(Boolean).join(" ") }; } function getRoute(connection) { const chains = Array.isArray(connection.chains) ? connection.chains : []; const routeTag = [...chains].reverse().find(getRouteDisplayNameByTag); const fallbackRouteTag = getRouteTagFromRule(connection.rule); const route = getRouteDisplayNameByTag(routeTag || "") || getRouteDisplayNameByTag(fallbackRouteTag) || normalizeString(routeTag) || normalizeString(fallbackRouteTag); return route || "-"; } function getNetwork(connection) { return normalizeString(connection.metadata?.network).toLowerCase() || "-"; } function sortConnections(connections, tab) { return [...connections].sort((a, b) => { if (tab === "closed") { return (b.closedAt || 0) - (a.closedAt || 0); } return parseStartedAt(b) - parseStartedAt(a); }); } function getConnectionsForActiveTab() { const source = activeTab === "active" ? Array.from(activeConnections.values()) : Array.from(closedConnections.values()); return sortConnections(source, activeTab); } function normalizeSearchValue(value) { return value.toLowerCase().replace(/\s+/g, " ").trim(); } function getSearchValues(connection) { const target = getTargetCellParts(connection); const source = getSourceCellParts(connection); return [ connection.id, target.primary, getNetwork(connection), getRoute(connection), formatConnectionDuration(connection), formatBytes2(connection.download), formatBytes2(connection.upload), source.primary, source.copyValue, source.searchValue ].filter(Boolean); } function getVisibleConnections() { const normalizedSearch = normalizeSearchValue(searchQuery); return getConnectionsForActiveTab().filter((connection) => { const sourceIp = getConnectionSourceIp(connection); const isMatchingDevice = selectedDeviceFilter === ALL_FILTER_VALUE || sourceIp === selectedDeviceFilter; let isMatchingRoute = selectedRouteFilter === ALL_FILTER_VALUE; if (!isMatchingRoute && getRoute(connection) === selectedRouteFilter) { isMatchingRoute = true; } if (!isMatchingDevice || !isMatchingRoute) { return false; } if (!normalizedSearch) { return true; } return getSearchValues(connection).some( (value) => normalizeSearchValue(value).includes(normalizedSearch) ); }); } function moveConnectionToClosed(connection, now) { closedConnections.set(connection.id, { ...connection, closedAt: now, lastSeenAt: now }); } function trimClosedConnections() { const sorted = sortConnections( Array.from(closedConnections.values()), "closed" ); sorted.slice(CLOSED_CONNECTION_LIMIT).forEach((connection) => { closedConnections.delete(connection.id); }); } function applyConnectionsPayload(payload) { if (monitoringPaused) { pendingConnectionsPayload = payload; return; } const mountId = monitoringMountId; const now = Date.now(); const incomingIds = /* @__PURE__ */ new Set(); const rawConnections = Array.isArray(payload.connections) ? payload.connections : []; rawConnections.forEach((rawConnection) => { const id = normalizeString(rawConnection.id); if (!id) { return; } incomingIds.add(id); closedConnections.delete(id); activeConnections.set(id, { ...rawConnection, id, lastSeenAt: now }); }); Array.from(activeConnections.entries()).forEach(([id, connection]) => { if (!incomingIds.has(id)) { activeConnections.delete(id); moveConnectionToClosed(connection, now); } }); trimClosedConnections(); loading = false; failed = false; if (monitoringMounted && mountId === monitoringMountId) { renderControls(); renderConnections2(); } } function setTab(tab) { if (activeTab === tab) { return; } activeTab = tab; renderControls(); renderConnections2(); } function getKnownSourceIps() { const ips = /* @__PURE__ */ new Set(); activeConnections.forEach((connection) => { const ip = getConnectionSourceIp(connection); if (ip) { ips.add(ip); } }); closedConnections.forEach((connection) => { const ip = getConnectionSourceIp(connection); if (ip) { ips.add(ip); } }); return Array.from(ips).sort((a, b) => { const byLabel = getDeviceFilterLabel(a).localeCompare( getDeviceFilterLabel(b) ); return byLabel || a.localeCompare(b); }); } function renderRouteFilterOptions() { const select = document.getElementById( "monitoring-route-filter" ); if (!select) return; const uniqueRoutes = /* @__PURE__ */ new Set(); Object.values(routeDisplayNames).forEach((name) => { if (name) uniqueRoutes.add(name); }); const routes = Array.from(uniqueRoutes).sort(); if (selectedRouteFilter !== ALL_FILTER_VALUE && !routes.includes(selectedRouteFilter)) { selectedRouteFilter = ALL_FILTER_VALUE; } const signature = [selectedRouteFilter, ...routes].join("|"); if (signature === lastRouteFilterSignature) { select.value = selectedRouteFilter; return; } lastRouteFilterSignature = signature; const options = [ E("option", { value: ALL_FILTER_VALUE }, _("All Routes")), ...routes.map((name) => E("option", { value: name }, name)) ]; select.replaceChildren(...options); select.value = selectedRouteFilter; } function renderDeviceFilterOptions() { const select = document.getElementById( "monitoring-device-filter" ); if (!select) { return; } const sourceIps = getKnownSourceIps(); if (selectedDeviceFilter !== ALL_FILTER_VALUE && !sourceIps.includes(selectedDeviceFilter)) { selectedDeviceFilter = ALL_FILTER_VALUE; } const signature = [ selectedDeviceFilter, ...sourceIps.map((ip) => `${ip}:${getDeviceFilterLabel(ip)}`) ].join("|"); if (signature === lastDeviceFilterSignature) { select.value = selectedDeviceFilter; return; } lastDeviceFilterSignature = signature; const options = [ E("option", { value: ALL_FILTER_VALUE }, _("All")), ...sourceIps.map( (ip) => E("option", { value: ip }, getDeviceFilterLabel(ip)) ) ]; select.replaceChildren(...options); select.value = selectedDeviceFilter; } function setButtonActive(button, active) { if (!button) { return; } button.classList.toggle("tachyon_monitoring-page__tab--active", active); } function renderTabButtonContent(label, count) { return [ E("span", { class: "tachyon_monitoring-page__tab-label" }, label), E("span", { class: "tachyon_monitoring-page__tab-badge" }, String(count)) ]; } function renderControls() { const activeButton = document.getElementById( "monitoring-tab-active" ); const closedButton = document.getElementById( "monitoring-tab-closed" ); const closeAllButton = document.getElementById( "monitoring-close-all" ); const pauseToggleButton = document.getElementById( "monitoring-pause-toggle" ); if (activeButton) { activeButton.replaceChildren( ...renderTabButtonContent(_("Active"), activeConnections.size) ); activeButton.disabled = serviceAvailability === "stopped"; } if (closedButton) { closedButton.replaceChildren( ...renderTabButtonContent(_("Closed"), closedConnections.size) ); closedButton.disabled = serviceAvailability === "stopped"; } setButtonActive(activeButton, activeTab === "active"); setButtonActive(closedButton, activeTab === "closed"); if (closeAllButton) { closeAllButton.replaceChildren(renderXIcon24()); closeAllButton.disabled = serviceAvailability === "stopped" || activeConnections.size === 0 || closingAll; } if (pauseToggleButton) { const title = monitoringPaused ? _("Resume updates") : _("Pause updates"); pauseToggleButton.replaceChildren( monitoringPaused ? renderPlayIcon24() : renderPauseIcon24() ); pauseToggleButton.title = title; pauseToggleButton.setAttribute("aria-label", title); pauseToggleButton.disabled = serviceAvailability === "stopped"; pauseToggleButton.classList.toggle( "tachyon_monitoring-page__icon-button--active", monitoringPaused ); } const searchIcon = document.querySelector( ".tachyon_monitoring-page__search-icon" ); if (searchIcon && searchIcon.childNodes.length === 0) { searchIcon.replaceChildren(renderSearchIcon24()); } renderDeviceFilterOptions(); renderRouteFilterOptions(); const select = document.getElementById( "monitoring-device-filter" ); const searchInput = document.getElementById( "monitoring-search" ); if (select) { select.disabled = serviceAvailability === "stopped"; } if (searchInput) { searchInput.disabled = serviceAvailability === "stopped"; } } function renderValue(value, className = "") { const text = value || "-"; const element = E( "span", { class: ["tachyon_monitoring-page__value", className].filter(Boolean).join(" "), title: text }, text ); element.setAttribute("data-copy-value", text); return element; } function renderSourceValue(source) { const fullText = source.copyValue || source.primary || "-"; if (!source.ip) { const element2 = E( "span", { class: "tachyon_monitoring-page__value tachyon_monitoring-page__source-value tachyon_monitoring-page__source-value--ip-only", title: fullText }, source.primary || "-" ); element2.setAttribute("data-copy-value", fullText); return element2; } const element = E( "span", { class: "tachyon_monitoring-page__value tachyon_monitoring-page__source-value", title: fullText }, [ E( "span", { class: "tachyon_monitoring-page__source-name" }, source.primary ), E("span", { class: "tachyon_monitoring-page__source-ip" }, source.ip) ] ); element.setAttribute("data-copy-value", fullText); return element; } function renderTableCell(label, children) { const cell = E("td", {}, children); cell.setAttribute("data-label", label); return cell; } function renderConnectionRow(connection) { const target = getTargetCellParts(connection); const source = getSourceCellParts(connection); const isClosing = closingConnectionIds.has(connection.id); const closeButton = activeTab === "active" ? E( "button", { class: "btn cbi-button tachyon_monitoring-page__row-action", title: _("Close connection"), "aria-label": _("Close connection"), type: "button", value: connection.id, ...isClosing ? { disabled: true } : {} }, [renderXIcon24()] ) : E("span", {}, "-"); return E( "tr", { class: isClosing ? "tachyon_monitoring-page__row--closing" : "" }, [ renderTableCell(_("Host"), [renderValue(target.primary)]), renderTableCell(_("Type"), [ renderValue(getNetwork(connection), "tachyon_monitoring-page__network") ]), renderTableCell(_("Route"), [ renderValue(getRoute(connection), "tachyon_monitoring-page__route") ]), renderTableCell(_("Time"), [ renderValue(formatConnectionDuration(connection)) ]), renderTableCell(_("Downloaded"), [ renderValue(formatBytes2(connection.download)) ]), renderTableCell(_("Uploaded"), [ renderValue(formatBytes2(connection.upload)) ]), renderTableCell(_("Source"), [renderSourceValue(source)]), renderTableCell(_("Close"), [closeButton]) ] ); } function renderStateRow(text, className = "") { return E("tr", { class: "tachyon_monitoring-page__state-row" }, [ E( "td", { class: "tachyon_monitoring-page__state-cell", colSpan: 8 }, [ E( "div", { class: ["tachyon_monitoring-page__state", className].filter(Boolean).join(" ") }, text ) ] ) ]); } function renderConnectionsTable(connections, state) { const rows = state ? [renderStateRow(state.text, state.className)] : connections.map(renderConnectionRow); return E("div", { class: "tachyon_monitoring-page__table-wrap" }, [ E( "table", { class: "table cbi-section-table tachyon_monitoring-page__table" }, [ E("thead", {}, [ E("tr", {}, [ E("th", {}, _("Host")), E("th", {}, _("Type")), E("th", {}, _("Route")), E("th", {}, _("Time")), E("th", {}, `\u2193 ${_("Downloaded")}`), E("th", {}, `\u2191 ${_("Uploaded")}`), E("th", {}, _("Source")), E("th", {}, _("Close")) ]) ]), E("tbody", {}, rows) ] ) ]); } function isNodeInsideMonitoring(node) { const root = document.getElementById("monitoring-status"); return Boolean(root && node && root.contains(node)); } function isTextSelectionInsideMonitoring() { const selection = window.getSelection?.(); if (!selection || selection.isCollapsed) { return false; } return isNodeInsideMonitoring(selection.anchorNode) || isNodeInsideMonitoring(selection.focusNode); } function renderConnections2(options = {}) { const container = document.getElementById("monitoring-connections"); if (!container) { return; } if (!options.force && isTextSelectionInsideMonitoring()) { renderSkippedForSelection = true; return; } renderSkippedForSelection = false; const previousScrollLeft = container.scrollLeft; if (serviceAvailability === "stopped") { container.replaceChildren( renderConnectionsTable([], { text: _( "Tachyon service is stopped. Start the service to display connections." ) }) ); return; } if (loading) { container.replaceChildren( renderConnectionsTable([], { text: _("Loading connections"), className: "tachyon_monitoring-page__state--loading" }) ); return; } if (failed) { container.replaceChildren( renderConnectionsTable([], { text: _("Connections are unavailable"), className: "tachyon_monitoring-page__state--error" }) ); return; } const visibleConnections = getVisibleConnections(); if (visibleConnections.length === 0) { container.replaceChildren( renderConnectionsTable([], { text: activeTab === "active" ? _("No active connections") : _("No closed connections") }) ); return; } container.replaceChildren(renderConnectionsTable(visibleConnections)); container.scrollLeft = previousScrollLeft; } function flushRenderAfterSelection() { if (!renderSkippedForSelection || isTextSelectionInsideMonitoring()) { return; } renderConnections2({ force: true }); } function setMonitoringPaused(paused) { if (monitoringPaused === paused) { return; } monitoringPaused = paused; monitoringPausedAt = paused ? Date.now() : null; renderSkippedForSelection = false; renderControls(); if (!paused) { const payload = pendingConnectionsPayload; pendingConnectionsPayload = null; if (payload) { applyConnectionsPayload(payload); return; } if (connectionsPollTimer) { void pollConnectionsSnapshot(); return; } } renderConnections2(); } function isElementOverflowing(element) { return element.scrollWidth > element.clientWidth + 1; } function getMonitoringValueOverflowElements(element) { return [ element, ...Array.from(element.querySelectorAll("*")) ].filter(isElementOverflowing); } function getElementCopyText(element, fallback) { return element.getAttribute("data-copy-value") || element.textContent || fallback; } function compactMonitoringText(value) { return value.replace(/\u2026/g, "").trim().replace(/\s+/g, ""); } function getMonitoringValueTextElements(element) { const children = Array.from(element.children).filter( (child) => child instanceof HTMLElement ); if (children.length === 0) { return [element]; } const textElements = children.flatMap(getMonitoringValueTextElements).filter((child) => compactMonitoringText(getElementCopyText(child, ""))); return textElements.length > 0 ? textElements : [element]; } function estimateVisibleMonitoringTextLength(element, fallbackText) { const text = compactMonitoringText(getElementCopyText(element, fallbackText)); if (!text) { return 0; } if (!isElementOverflowing(element)) { return text.length; } return Math.floor( element.clientWidth / Math.max(element.scrollWidth, 1) * text.length ); } function getEstimatedVisibleMonitoringTextLength(element, fallbackText) { const textElements = getMonitoringValueTextElements(element); if (textElements.length === 1 && textElements[0] === element) { return estimateVisibleMonitoringTextLength(element, fallbackText); } return textElements.reduce( (total, textElement) => total + estimateVisibleMonitoringTextLength(textElement, fallbackText), 0 ); } function isCompactTextSubsequence(needle, haystack) { let haystackIndex = 0; for (let needleIndex = 0; needleIndex < needle.length; needleIndex += 1) { haystackIndex = haystack.indexOf(needle[needleIndex], haystackIndex); if (haystackIndex === -1) { return false; } haystackIndex += 1; } return true; } function getSelectionValueElements(selection) { const root = document.getElementById("monitoring-status"); if (!root) { return []; } return Array.from( root.querySelectorAll( ".tachyon_monitoring-page__value[data-copy-value]" ) ).filter((element) => { for (let index = 0; index < selection.rangeCount; index += 1) { try { if (selection.getRangeAt(index).intersectsNode(element)) { return true; } } catch (_error) { return false; } } return false; }); } function shouldCopyFullMonitoringValue(element, selectedText, fullText) { const normalizedSelectedText = selectedText.replace(/\u2026/g, "").trim(); const normalizedFullText = fullText.trim(); const compactSelectedText = compactMonitoringText(selectedText); const compactFullText = compactMonitoringText(fullText); const overflowElements = getMonitoringValueOverflowElements(element); const hasCompositeText = getMonitoringValueTextElements(element).length > 1; if (!normalizedSelectedText || !normalizedFullText) { return false; } if (normalizedSelectedText === normalizedFullText) { return true; } if (overflowElements.length === 0) { return false; } if (hasCompositeText) { const selectedPrefix = compactSelectedText.slice( 0, Math.min(4, compactSelectedText.length) ); if (!compactFullText.startsWith(selectedPrefix) || !isCompactTextSubsequence(compactSelectedText, compactFullText)) { return false; } } else if (!compactFullText.startsWith(compactSelectedText)) { return false; } const estimatedVisibleChars = getEstimatedVisibleMonitoringTextLength( element, normalizedFullText ); return compactSelectedText.length >= Math.max(4, estimatedVisibleChars - 2); } function handleMonitoringValueCopy(event) { const selection = window.getSelection?.(); if (!selection || selection.isCollapsed) { return; } const valueElements = getSelectionValueElements(selection); if (valueElements.length !== 1) { return; } const valueElement = valueElements[0]; const fullText = valueElement.getAttribute("data-copy-value") || valueElement.textContent || ""; const selectedText = selection.toString(); if (!shouldCopyFullMonitoringValue(valueElement, selectedText, fullText)) { return; } event.clipboardData?.setData("text/plain", fullText); event.preventDefault(); } async function closeConnection(connectionId) { if (!connectionId || closingConnectionIds.has(connectionId)) { return; } closingConnectionIds.add(connectionId); renderConnections2(); try { const response = await TachyonShellMethods.closeClashApiConnection(connectionId); if (!response.success) { showToast(_("Failed to close connection"), "error"); return; } const now = Date.now(); const connection = activeConnections.get(connectionId); if (connection) { activeConnections.delete(connectionId); moveConnectionToClosed(connection, now); trimClosedConnections(); pendingConnectionsPayload = null; renderControls(); } } catch (error) { logger.error("[MONITORING]", "closeConnection: failed", error); showToast(_("Failed to close connection"), "error"); } finally { closingConnectionIds.delete(connectionId); renderConnections2(); } } async function closeAllConnections() { if (activeConnections.size === 0 || closingAll) { return; } closingAll = true; renderControls(); try { const response = await TachyonShellMethods.closeAllClashApiConnections(); if (!response.success) { showToast(_("Failed to close connections"), "error"); return; } const now = Date.now(); activeConnections.forEach((connection) => { moveConnectionToClosed(connection, now); }); activeConnections.clear(); pendingConnectionsPayload = null; trimClosedConnections(); } catch (error) { logger.error("[MONITORING]", "closeAllConnections: failed", error); showToast(_("Failed to close connections"), "error"); } finally { closingAll = false; renderControls(); renderConnections2(); } } function bindControls() { const activeButton = document.getElementById("monitoring-tab-active"); const closedButton = document.getElementById("monitoring-tab-closed"); const select = document.getElementById( "monitoring-device-filter" ); const routeSelect = document.getElementById( "monitoring-route-filter" ); const searchInput = document.getElementById( "monitoring-search" ); const closeAllButton = document.getElementById("monitoring-close-all"); const pauseToggleButton = document.getElementById("monitoring-pause-toggle"); const connectionsContainer = document.getElementById( "monitoring-connections" ); if (activeButton) { activeButton.onclick = () => setTab("active"); } if (closedButton) { closedButton.onclick = () => setTab("closed"); } if (closeAllButton) { closeAllButton.onclick = () => { void closeAllConnections(); }; } if (pauseToggleButton) { pauseToggleButton.onclick = () => { setMonitoringPaused(!monitoringPaused); pauseToggleButton.blur(); }; } if (select) { select.onchange = () => { selectedDeviceFilter = select.value || ALL_FILTER_VALUE; renderConnections2(); }; } if (routeSelect) { routeSelect.onchange = () => { selectedRouteFilter = routeSelect.value || ALL_FILTER_VALUE; renderConnections2(); }; } if (searchInput) { searchInput.oninput = () => { searchQuery = searchInput.value; renderConnections2(); }; } if (connectionsContainer) { connectionsContainer.onclick = (event) => { const target = event.target; const button = target?.closest( ".tachyon_monitoring-page__row-action" ); if (button?.value) { void closeConnection(button.value); } }; } } async function loadLocalDevices() { try { localDeviceChoices = await dependencies.loadLocalDeviceChoices?.() || {}; } catch (error) { logger.warn("[MONITORING]", "loadLocalDevices: failed", error); localDeviceChoices = {}; } finally { renderControls(); renderConnections2(); } } async function loadRouteDisplayNames() { try { buildRouteDisplayNames(await CustomTachyonMethods.getConfigSections()); } catch (error) { logger.warn("[MONITORING]", "loadRouteDisplayNames: failed", error); buildRouteDisplayNames([]); } finally { renderControls(); renderConnections2(); } } async function pollConnectionsSnapshot() { if (pollingConnections || !monitoringMounted || monitoringPaused || serviceAvailability !== "running") { return; } const mountId = monitoringMountId; pollingConnections = true; try { const response = await TachyonShellMethods.getClashApiConnections(); if (!monitoringMounted || mountId !== monitoringMountId || serviceAvailability !== "running") { return; } if (!response.success) { failed = true; loading = false; renderConnections2(); return; } applyConnectionsPayload(normalizeConnectionsPayload(response.data)); } catch (error) { if (!monitoringMounted || mountId !== monitoringMountId || serviceAvailability !== "running") { return; } logger.error("[MONITORING]", "connections polling failed", error); failed = true; loading = false; renderConnections2(); } finally { pollingConnections = false; } } function startConnectionsPolling() { if (connectionsPollTimer) { return; } void pollConnectionsSnapshot(); connectionsPollTimer = setInterval(() => { void pollConnectionsSnapshot(); }, CONNECTIONS_RPC_POLL_INTERVAL_MS); } async function connectToConnectionsSocket(updatesId) { const mountId = monitoringMountId; const clashApiSecret = await getClashApiSecret2(); if (!monitoringMounted || mountId !== monitoringMountId || updatesId !== connectionsUpdatesId || serviceAvailability !== "running") { return; } connectionsSocketUrl = `${getClashWsUrl()}/connections?token=${clashApiSecret}`; socket.subscribe( connectionsSocketUrl, (msg) => { if (updatesId !== connectionsUpdatesId || serviceAvailability !== "running") { return; } try { applyConnectionsPayload(JSON.parse(msg)); } catch (error) { logger.error("[MONITORING]", "connections socket parse failed", error); } }, (_err) => { if (!monitoringMounted || mountId !== monitoringMountId || updatesId !== connectionsUpdatesId || serviceAvailability !== "running") { return; } failed = true; loading = false; renderConnections2(); } ); } function startConnectionsUpdates() { if (serviceAvailability !== "running") { return; } if (canUseDirectClashApi()) { const updatesId = ++connectionsUpdatesId; void connectToConnectionsSocket(updatesId); return; } startConnectionsPolling(); } function stopConnectionsUpdates() { connectionsUpdatesId += 1; if (connectionsPollTimer) { clearInterval(connectionsPollTimer); connectionsPollTimer = null; } if (connectionsSocketUrl) { socket.disconnect(connectionsSocketUrl); connectionsSocketUrl = ""; } } function setServiceAvailability(next) { if (serviceAvailability === next) { return; } serviceAvailability = next; if (next === "running") { loading = true; failed = false; startConnectionsUpdates(); } else { stopConnectionsUpdates(); pendingConnectionsPayload = null; if (next === "stopped") { loading = false; failed = false; activeConnections.clear(); closedConnections.clear(); closingConnectionIds.clear(); } else if (next === "unavailable") { loading = false; failed = true; } } renderControls(); renderConnections2(); } function watchServiceState() { serviceStateUnsubscribe?.(); serviceStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (!monitoringMounted) { return; } setServiceAvailability( getServiceAvailability({ loading: false, failed: false, running: uiState.service.tachyon.running }) ); }); } function resetMonitoringState() { activeTab = "active"; selectedDeviceFilter = ALL_FILTER_VALUE; selectedRouteFilter = ALL_FILTER_VALUE; searchQuery = ""; lastDeviceFilterSignature = ""; lastRouteFilterSignature = ""; loading = true; failed = false; closingAll = false; monitoringPaused = false; monitoringPausedAt = null; serviceAvailability = "loading"; pendingConnectionsPayload = null; activeConnections.clear(); closedConnections.clear(); closingConnectionIds.clear(); const searchInput = document.getElementById( "monitoring-search" ); if (searchInput) { searchInput.value = ""; } } async function onPageMount3() { onPageUnmount3(); monitoringMounted = true; monitoringMountId += 1; const mountId = monitoringMountId; resetMonitoringState(); bindControls(); renderControls(); renderConnections2(); watchServiceState(); void loadLocalDevices(); void loadRouteDisplayNames(); if (getCachedRuntimeUiState()) { void refreshRuntimeUiState({ force: true }); } else { const uiState = await refreshRuntimeUiState({ force: true }); if (!monitoringMounted || mountId !== monitoringMountId) { return; } if (!uiState && serviceAvailability === "loading") { setServiceAvailability("unavailable"); } } document.addEventListener("selectionchange", flushRenderAfterSelection); document.addEventListener("copy", handleMonitoringValueCopy); renderTimer = setInterval(() => { if (monitoringPaused) { return; } renderConnections2(); }, RENDER_INTERVAL_MS); } function onPageUnmount3() { monitoringMounted = false; monitoringMountId += 1; if (renderTimer) { clearInterval(renderTimer); renderTimer = null; } stopConnectionsUpdates(); serviceStateUnsubscribe?.(); serviceStateUnsubscribe = null; document.removeEventListener("selectionchange", flushRenderAfterSelection); document.removeEventListener("copy", handleMonitoringValueCopy); } function registerLifecycleListeners3() { if (monitoringLifecycleRegistered) { return; } monitoringLifecycleRegistered = true; store.subscribe( (next, prev, diff) => { if (diff.tabService && next.tabService.current !== prev.tabService.current) { const isMonitoringVisible = next.tabService.current === "monitoring"; if (isMonitoringVisible) { return onPageMount3(); } if (!isMonitoringVisible) { return onPageUnmount3(); } } } ); } async function initController3(controllerDependencies = {}) { dependencies = { ...dependencies, ...controllerDependencies }; if (monitoringControllerInitialized) { return; } monitoringControllerInitialized = true; onMount("monitoring-status").then(() => { registerLifecycleListeners3(); if (store.get().tabService.current === "monitoring") { onPageMount3(); } }); } // src/tachyon/tabs/monitoring/styles.ts var styles5 = ` .tachyon_monitoring-page { --tachyon-monitoring-danger-color: #d9534f; } #cbi-${TACHYON_UCI_PACKAGE}-monitoring-_mount_node { margin: 16px 0 22px; padding: 0; } #cbi-${TACHYON_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-title { display: none; } #cbi-${TACHYON_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-field { margin-left: 0; width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-monitoring-_mount_node > div { width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-monitoring > h3 { display: none; } .tachyon_monitoring-page { --tachyon-monitoring-control-height: 34px; --tachyon-monitoring-row-action-size: 24px; --tachyon-monitoring-divider-color: rgba(127, 127, 127, 0.22); --tachyon-monitoring-soft-bg: rgba(127, 127, 127, 0.08); --tachyon-monitoring-soft-bg-hover: rgba(127, 127, 127, 0.14); --tachyon-monitoring-danger-color: var(--error-color-medium, #d32f2f); --tachyon-monitoring-success-color: var(--success-color-medium, #2e7d32); --tachyon-monitoring-paused-color: var(--primary-color-high, #1976d2); width: 100%; min-width: 0; } .tachyon_monitoring-page__panel { margin-top: 0; border: 0; border-radius: 0; padding: 0; background: transparent; box-sizing: border-box; width: 100%; min-width: 0; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__icon-button { width: 32px; height: 32px; min-width: 32px; min-height: 32px; padding: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; line-height: 1; margin: 0; border: 1px solid var(--tachyon-monitoring-divider-color) !important; border-radius: 6px; background: var(--tachyon-monitoring-soft-bg) !important; color: var(--text-color-medium) !important; box-shadow: none; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__icon-button:hover:not(:disabled) { background: var(--tachyon-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__icon-button--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__icon-button:disabled { opacity: 0.45; cursor: not-allowed; } .tachyon_monitoring-page #monitoring-close-all.btn.tachyon_monitoring-page__icon-button { order: 2; border-color: rgba(217, 83, 79, 0.4) !important; background: transparent !important; color: var(--tachyon-monitoring-danger-color) !important; } .tachyon_monitoring-page #monitoring-close-all.btn.tachyon_monitoring-page__icon-button:hover:not(:disabled) { border-color: rgba(217, 83, 79, 0.6) !important; background: transparent !important; color: color-mix(in srgb, var(--tachyon-monitoring-danger-color) 70%, white) !important; } .tachyon_monitoring-page #monitoring-pause-toggle.btn.tachyon_monitoring-page__icon-button, .tachyon_monitoring-page #monitoring-pause-toggle.btn.tachyon_monitoring-page__icon-button--active { order: 1; border-color: rgba(128, 128, 128, 0.3) !important; background: transparent !important; color: var(--text-color-medium, #888) !important; } .tachyon_monitoring-page #monitoring-pause-toggle.btn.tachyon_monitoring-page__icon-button:hover:not(:disabled), .tachyon_monitoring-page #monitoring-pause-toggle.btn.tachyon_monitoring-page__icon-button--active:hover:not(:disabled) { border-color: rgba(128, 128, 128, 0.6) !important; background: transparent !important; color: var(--text-color-high, #eee) !important; } .tachyon_monitoring-page__icon-button svg, .tachyon_monitoring-page__row-action svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } .tachyon_monitoring-page__controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; width: 100%; min-width: 0; } .tachyon_monitoring-page__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; } .tachyon_monitoring-page__tabs { display: inline-flex; align-items: center; gap: 2px; width: max-content; padding: 2px; border: 1px solid var(--tachyon-monitoring-divider-color); border-radius: 6px; background: var(--tachyon-monitoring-soft-bg); box-sizing: border-box; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__tab { height: calc(var(--tachyon-monitoring-control-height) - 6px); min-height: calc(var(--tachyon-monitoring-control-height) - 6px); margin: 0; padding: 0 12px; border: 0 !important; border-radius: 4px; background: transparent !important; color: var(--text-color-medium) !important; box-shadow: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 600; line-height: 1; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__tab:hover { background: var(--tachyon-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__tab--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; font-weight: 700; } .tachyon_monitoring-page__tab-label { display: inline-block; } .tachyon_monitoring-page__tab-badge { min-width: 18px; height: 18px; padding: 0 6px; border-radius: 999px; background: rgba(127, 127, 127, 0.22); color: var(--text-color-medium); display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; font-size: 12px; font-weight: 700; line-height: 1; } .tachyon_monitoring-page__tab--active .tachyon_monitoring-page__tab-badge { background: rgba(25, 118, 210, 0.22); color: var(--primary-color-high, #1976d2); } .tachyon_monitoring-page__filters { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; } .tachyon_monitoring-page__device-filter { width: min(220px, 100%); min-width: 0; height: var(--tachyon-monitoring-control-height) !important; min-height: var(--tachyon-monitoring-control-height) !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; box-sizing: border-box; line-height: calc(var(--tachyon-monitoring-control-height) - 2px) !important; } .tachyon_monitoring-page__search { position: relative; display: flex; align-items: center; width: min(320px, 100%); min-width: 0; height: var(--tachyon-monitoring-control-height); margin: 0; } .tachyon_monitoring-page__search-icon { position: absolute; left: 8px; width: 16px; height: 16px; color: var(--text-color-medium); pointer-events: none; } .tachyon_monitoring-page__search-icon svg { width: 16px; height: 16px; display: block; } .tachyon_monitoring-page__search-input { width: 100%; height: var(--tachyon-monitoring-control-height) !important; min-height: var(--tachyon-monitoring-control-height) !important; padding-left: 30px !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; box-sizing: border-box; line-height: calc(var(--tachyon-monitoring-control-height) - 2px) !important; } .tachyon_monitoring-page__body { margin-top: 0; width: 100%; min-width: 0; } .tachyon_monitoring-page__table-wrap { width: 100%; overflow-x: auto; margin-bottom: 0; } .tachyon_monitoring-page__table { width: 100%; min-width: 840px; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 0; } .tachyon_monitoring-page__table th, .tachyon_monitoring-page__table td { padding: 8px 6px; border-bottom: 1px solid var(--tachyon-monitoring-divider-color); box-sizing: border-box; text-align: left; vertical-align: middle; overflow: hidden; white-space: nowrap; } .tachyon_monitoring-page__table th { color: var(--text-color-medium); font-size: 11px; font-weight: 600; text-transform: uppercase; white-space: nowrap; border-bottom-color: rgba(127, 127, 127, 0.32); } .tachyon_monitoring-page__table th:nth-child(1) { width: 28%; } .tachyon_monitoring-page__table th:nth-child(2) { width: 6%; } .tachyon_monitoring-page__table th:nth-child(3) { width: 16%; } .tachyon_monitoring-page__table th:nth-child(4) { width: 8%; } .tachyon_monitoring-page__table th:nth-child(5) { width: 9.5%; } .tachyon_monitoring-page__table th:nth-child(6) { width: 8.5%; } .tachyon_monitoring-page__table th:nth-child(7) { width: 16%; } .tachyon_monitoring-page__table th:nth-child(8) { width: 8%; } .tachyon_monitoring-page__table tbody tr:last-child td { border-bottom: 0; } .tachyon_monitoring-page__table td:last-child { padding-top: 0; padding-bottom: 0; } .tachyon_monitoring-page__table th:nth-child(4), .tachyon_monitoring-page__table td:nth-child(4), .tachyon_monitoring-page__table th:nth-child(5), .tachyon_monitoring-page__table td:nth-child(5), .tachyon_monitoring-page__table th:nth-child(6), .tachyon_monitoring-page__table td:nth-child(6) { text-align: right; } .tachyon_monitoring-page__table th:nth-child(7), .tachyon_monitoring-page__table td:nth-child(7) { text-align: left; } .tachyon_monitoring-page__table th:last-child, .tachyon_monitoring-page__table td:last-child { text-align: center; } .tachyon_monitoring-page__value { display: block; max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; line-height: 1.3; color: var(--text-color-high); font-size: 13px; user-select: text; } .tachyon_monitoring-page__source-value { display: flex; align-items: baseline; justify-content: flex-start; gap: 5px; } .tachyon_monitoring-page__source-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tachyon_monitoring-page__source-ip { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-color-medium); font-size: 12px; } .tachyon_monitoring-page__source-value--ip-only { color: var(--text-color-high); } .tachyon_monitoring-page__cell-main { color: var(--text-color-high); font-weight: 600; line-height: 1.25; } .tachyon_monitoring-page__cell-secondary { margin-top: 2px; color: var(--text-color-medium); font-size: 12px; line-height: 1.25; } .tachyon_monitoring-page__route { display: inline-block; width: auto; padding: 2px 6px; border-radius: 4px; background: rgba(128, 128, 128, 0.15); color: var(--text-color-high, #eee); font-size: 11px; font-weight: 500; } .tachyon_monitoring-page__network { background: transparent; border: 0; padding: 0; color: var(--text-color-medium, #bbb); font-family: inherit; font-size: 13px; text-transform: lowercase; } .tachyon_monitoring-page__table td:nth-child(4) .tachyon_monitoring-page__value, .tachyon_monitoring-page__table td:nth-child(5) .tachyon_monitoring-page__value, .tachyon_monitoring-page__table td:nth-child(6) .tachyon_monitoring-page__value { color: var(--text-color-medium, #bbb); font-family: inherit; text-align: right; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__row-action { width: var(--tachyon-monitoring-row-action-size); height: var(--tachyon-monitoring-row-action-size); min-width: var(--tachyon-monitoring-row-action-size); min-height: var(--tachyon-monitoring-row-action-size); padding: 0; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; line-height: 1; margin: 0; border: 0 !important; border-radius: 999px; background: transparent !important; color: var(--tachyon-monitoring-danger-color) !important; box-shadow: none; cursor: pointer; } .tachyon_monitoring-page__row-action svg { width: 14px; height: 14px; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__row-action:hover:not(:disabled) { background: var(--tachyon-monitoring-soft-bg-hover) !important; color: var(--tachyon-monitoring-danger-color) !important; } .tachyon_monitoring-page .btn.tachyon_monitoring-page__row-action:disabled { opacity: 0.45; cursor: wait; } .tachyon_monitoring-page__row--closing { opacity: 0.55; } .tachyon_monitoring-page__state { min-height: 90px; width: 100%; display: flex; align-items: center; justify-content: center; color: var(--text-color-medium); text-align: center; box-sizing: border-box; } .tachyon_monitoring-page__state-cell { padding: 0 !important; } .tachyon_monitoring-page__state--error { color: var(--error-color-medium, #d32f2f); } @media (max-width: 900px) { .tachyon_monitoring-page__controls { align-items: center; } .tachyon_monitoring-page__tabs { flex: 1 0 100%; } .tachyon_monitoring-page__filters { flex: 1 1 0; } .tachyon_monitoring-page__device-filter, .tachyon_monitoring-page__search { max-width: none; } .tachyon_monitoring-page__table { min-width: 0; } .tachyon_monitoring-page__table thead { display: none; } .tachyon_monitoring-page__table, .tachyon_monitoring-page__table tbody, .tachyon_monitoring-page__table tr, .tachyon_monitoring-page__table td { display: block; width: 100%; } .tachyon_monitoring-page__table tr { border: 1px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 8px; box-sizing: border-box; margin-bottom: 8px; } .tachyon_monitoring-page__table td { display: grid; grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); gap: 8px; border: 0; border-bottom: 1px solid var(--tachyon-monitoring-divider-color); padding: 4px 0; box-sizing: border-box; text-align: left; } .tachyon_monitoring-page__table td::before { content: attr(data-label); color: var(--text-color-medium); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tachyon_monitoring-page__table td:last-child { grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); align-items: center; border-bottom: 0; min-height: var(--tachyon-monitoring-row-action-size); padding: 0; } .tachyon_monitoring-page__value { text-align: right; } .tachyon_monitoring-page__source-value { justify-content: flex-end; } .tachyon_monitoring-page__state-row td::before { display: none; } } @media (max-width: 520px) { .tachyon_monitoring-page__controls, .tachyon_monitoring-page__filters { align-items: stretch; } .tachyon_monitoring-page__tabs, .tachyon_monitoring-page__filters, .tachyon_monitoring-page__device-filter, .tachyon_monitoring-page__search { width: 100%; } .tachyon_monitoring-page__controls, .tachyon_monitoring-page__filters { flex-direction: column; } .tachyon_monitoring-page__actions { align-self: flex-end; } .tachyon_monitoring-page__tabs { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); width: 100%; } .tachyon_monitoring-page__table td { grid-template-columns: 1fr; gap: 2px; } .tachyon_monitoring-page__value { text-align: left; } .tachyon_monitoring-page__source-value { justify-content: flex-start; } } `; // src/tachyon/tabs/monitoring/index.ts var MonitoringTab = { render: render3, initController: initController3, styles: styles5 }; // src/tachyon/tabs/updates/render.ts function render4() { return E("div", { id: "updates-status", class: "tachyon_updates-page" }, [ E("div", { id: "tachyon_updates-components", class: "tachyon_updates-page__components" }) ]); } // src/tachyon/tabs/updates/componentActionCompletion.ts function shouldApplyCompletedComponentActionResult(result, notify) { return result.action !== "check_update" || notify; } // src/tachyon/tabs/updates/checkResultLifecycle.ts function shouldPreserveCompletedCheckResultOnNextMount({ action, mounted }) { return action === "check_update" && !mounted; } function shouldResetCheckResultsOnMount({ anyActionLoading, preserveCheckResultsOnNextMount: preserveCheckResultsOnNextMount2, persistentCacheEnabled = false }) { return !persistentCacheEnabled && !anyActionLoading && !preserveCheckResultsOnNextMount2; } function shouldRefreshComponentStateBeforeRender(uiState) { return Boolean( uiState?.actions.component.some((state) => state.running === true) ); } function shouldExposeCheckResults({ mounted, cacheResolved }) { return mounted && cacheResolved; } // src/tachyon/tabs/updates/sameReleaseBuild.ts function describeSameReleaseBuild({ currentSha, latestSha }) { const current = (currentSha || "").substring(0, 8); const latest = (latestSha || "").substring(0, 8); if (current && latest) { return { kind: "sha", text: `${current} \u2192 ${latest}` }; } if (current || latest) { return { kind: "sha", text: current || latest }; } return { kind: "fallback", text: "" }; } // src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts function formatSha(sha) { if (!sha) return ""; const clean = sha.trim(); return clean.length >= 7 ? clean.slice(0, 7) : clean; } function renderVersionBadgeText(opts) { const cVer = opts.currentVersion || ""; const tVer = opts.targetVersion || ""; const cSha = formatSha(opts.currentSha); const tSha = formatSha(opts.targetSha); if (tVer) { if (cVer === tVer) { if (cSha && tSha && cSha !== tSha) { return `${cVer} (${cSha} \u2192 ${tSha})`; } if (tSha) { return `${cVer} (${tSha})`; } if (cSha) { return `${cVer} (${cSha})`; } return `${cVer} \u2192 ${tVer}`; } const currentPart = cVer ? cSha ? `${cVer} (${cSha})` : cVer : ""; const targetPart = tSha ? `${tVer} (${tSha})` : tVer; return currentPart ? `${currentPart} \u2192 ${targetPart}` : targetPart; } if (cVer) { return cSha ? `${cSha}` : cVer; } return opts.action; } var activeModalController = null; var activeModalJobId = null; function getActiveProgressModalController() { return activeModalController; } function setActiveProgressModalJobId(jobId) { activeModalJobId = jobId; } function showUpdateProgressModal(options) { if (activeModalController) { activeModalController.close(); } const isCheckAction = options.action === "check_update"; let modalTitleText = _("Operation in progress..."); if (isCheckAction) { modalTitleText = `${_("Checking for updates...")}: ${options.componentTitle}`; } else if (options.action === "remove") { modalTitleText = `${_("Removing")} ${options.componentTitle}...`; } else { modalTitleText = `${_("Updating")} ${options.componentTitle}...`; } let elapsedSeconds = 0; let timerInterval = null; let currentModalVersions = { ...options }; const timerBadgeEl = E( "div", { class: "tachyon-update-modal__timer-badge" }, "\u23F1\uFE0F 00:00" ); const titleBadgeEl = E( "span", { class: "tachyon-update-modal__version-badge" }, renderVersionBadgeText(options) ); const headerEl = E("div", { class: "tachyon-update-modal__header" }, [ E("div", { class: "tachyon-update-modal__header-info" }, [ E( "b", { class: "tachyon-update-modal__component-name" }, options.componentTitle ), titleBadgeEl ]), timerBadgeEl ]); const logPreEl = E("pre", { class: "tachyon-update-modal__log" }); const logPanelEl = E("div", { class: "tachyon-update-modal__log-panel" }, [ E("div", { class: "tachyon-update-modal__log-header" }, [ E("b", {}, _("Operation log")), renderButton({ classNames: ["cbi-button-action", "tachyon-update-modal__log-copy"], text: _("Copy log"), onClick: copyLog }) ]), logPreEl ]); const actionButtonContainer = E( "div", { class: "tachyon-update-modal__actions" }, [ renderButton({ text: _("Operation in progress..."), disabled: true, loading: true, onClick: () => { } }) ] ); const modalBodyEl = E("div", { class: "tachyon-update-modal__body" }, [ headerEl, logPanelEl, actionButtonContainer ]); const updateTimerDisplay = () => { const mins = Math.floor(elapsedSeconds / 60).toString().padStart(2, "0"); const secs = (elapsedSeconds % 60).toString().padStart(2, "0"); timerBadgeEl.textContent = `\u23F1\uFE0F ${mins}:${secs}`; }; timerInterval = setInterval(() => { elapsedSeconds += 1; updateTimerDisplay(); }, 1e3); function cleanupTimers() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } } let logTrackingJobId = null; let logTrackingOffset = 0; let logFullText = ""; let logPollTimer = null; let logPollStopped = true; let autoCompletedFromLog = false; function appendLogText(text) { if (!text) { return; } logFullText += text; logPreEl.textContent = logFullText; logPreEl.scrollTop = logPreEl.scrollHeight; if (!autoCompletedFromLog && options.component === "tachyon" && (options.action === "install" || options.action === "reinstall")) { if (logFullText.includes("Tachyon updated to") || logFullText.includes("Start Tachyon")) { autoCompletedFromLog = true; window.setTimeout(() => { if (activeModalController === controller) { controller.completeSuccess(_("Tachyon updated successfully!"), { reloadPage: true }); } }, 1e3); } } } async function pollLog() { if (logPollStopped || !logTrackingJobId) { return; } const jobId = logTrackingJobId; const offset = logTrackingOffset; try { const response = await TachyonShellMethods.componentActionLog( jobId, offset ); if (logPollStopped || logTrackingJobId !== jobId) { return; } if (response.success && response.data) { logTrackingOffset = response.data.offset; appendLogText(response.data.log); } } catch (_error) { } finally { if (!logPollStopped && logTrackingJobId === jobId) { logPollTimer = setTimeout(pollLog, 1500); } } } function stopLogTracking() { logPollStopped = true; if (logPollTimer) { clearTimeout(logPollTimer); logPollTimer = null; } logTrackingJobId = null; } function finishLogTracking() { if (logPollStopped || !logTrackingJobId) { stopLogTracking(); return; } const jobId = logTrackingJobId; const offset = logTrackingOffset; logPollStopped = true; if (logPollTimer) { clearTimeout(logPollTimer); logPollTimer = null; } void TachyonShellMethods.componentActionLog(jobId, offset).then((response) => { if (response.success && response.data) { logTrackingOffset = response.data.offset; appendLogText(response.data.log); } }).catch(() => { }).finally(() => { logTrackingJobId = null; }); } function copyLog() { if (!logFullText) { return; } copyToClipboard(logFullText); } const controller = { updateStep: (_stepIndex, _statusText) => { }, updateStatus: (_statusText) => { }, updateVersions: (opts) => { currentModalVersions = { ...currentModalVersions, ...opts }; titleBadgeEl.textContent = renderVersionBadgeText(currentModalVersions); }, completeSuccess: (message, opts) => { cleanupTimers(); finishLogTracking(); const successMsg = message || (isCheckAction ? _("Check completed!") : options.action === "remove" ? _("Removal completed successfully!") : _("Update completed successfully!")); actionButtonContainer.replaceChildren( E("div", { class: "tachyon-update-modal__success-banner" }, [ renderCheckIcon24(), E("span", {}, successMsg) ]) ); if (opts?.reloadPage) { let secondsLeft = 3; const reloadBtnText = () => `${_("Reloading page in")} ${secondsLeft}s...`; const closeBtn = renderButton({ classNames: ["cbi-button-save"], text: reloadBtnText(), onClick: () => { window.location.reload(); } }); actionButtonContainer.replaceChildren(closeBtn); const reloadTimer = setInterval(() => { secondsLeft -= 1; if (secondsLeft <= 0) { clearInterval(reloadTimer); window.location.reload(); } else { closeBtn.textContent = reloadBtnText(); } }, 1e3); } else { const actionButtons = []; if (opts?.onInstall) { const installBtn = renderButton({ classNames: ["cbi-button-save"], text: opts.installText || _("Install"), onClick: () => { controller.close(); opts.onInstall(); } }); actionButtons.push(installBtn); } const closeBtn = renderButton({ classNames: ["cbi-button-apply"], text: _("Close"), onClick: () => { controller.close(); } }); actionButtons.push(closeBtn); actionButtonContainer.replaceChildren(...actionButtons); const defaultAutoCloseMs = opts?.onInstall ? 0 : isCheckAction ? 1200 : 1200; const autoCloseMs = opts?.autoCloseMs ?? defaultAutoCloseMs; if (autoCloseMs > 0) { setTimeout(() => { if (activeModalController === controller || activeModalController === null) { controller.close(); } }, autoCloseMs); } } }, completeError: (errorMessage) => { cleanupTimers(); finishLogTracking(); actionButtonContainer.replaceChildren( E("div", { class: "tachyon-update-modal__error-banner" }, [ renderXIcon24(), E("span", {}, errorMessage || _("Operation failed")) ]) ); const closeBtn = renderButton({ classNames: ["cbi-button-remove"], text: _("Close"), onClick: () => { controller.close(); } }); actionButtonContainer.appendChild(closeBtn); }, startLogTracking: (jobId) => { if (!jobId || logTrackingJobId === jobId) { return; } logTrackingJobId = jobId; logTrackingOffset = 0; logPollStopped = false; if (logPollTimer) { clearTimeout(logPollTimer); logPollTimer = null; } void pollLog(); }, stopLogTracking: () => { stopLogTracking(); }, getLogText: () => logFullText, close: () => { cleanupTimers(); stopLogTracking(); if (activeModalController === controller) { activeModalController = null; activeModalJobId = null; } ui.hideModal(); } }; activeModalController = controller; ui.showModal(modalTitleText, modalBodyEl); return controller; } // src/tachyon/tabs/updates/initController.ts function getComponentCardTitle(component) { switch (component) { case "tachyon": return "Tachyon"; case "sing_box": return "Sing-box"; case "zapret": return "Zapret"; case "zapret2": return "Zapret2"; case "byedpi": return "ByeDPI"; default: return String(component); } } function getComponentCurrentVersion(component) { const sys = store.get().diagnosticsSystemInfo; switch (component) { case "tachyon": return sys.tachyon_version; case "sing_box": return sys.sing_box_version; case "zapret": return sys.zapret_version; case "zapret2": return sys.zapret2_version; case "byedpi": return sys.byedpi_version; default: return void 0; } } var updatesLifecycleRegistered = false; var updatesControllerInitialized = false; var updatesMounted = false; var updatesMountId = 0; var pageUnloading2 = false; var preserveCheckResultsOnNextMount = false; var componentUpdateCheckCacheResolved = false; var componentUpdateCheckCacheSnapshot = null; var componentUpdateCheckCachePromise = null; var componentActionStateUnsubscribe = null; var componentActionStateRefreshPromise = null; var followedComponentJobs = /* @__PURE__ */ new Set(); var handledComponentJobs = /* @__PURE__ */ new Set(); if (typeof window !== "undefined") { window.addEventListener("pagehide", () => { pageUnloading2 = true; }); window.addEventListener("pageshow", () => { pageUnloading2 = false; }); } function isNotInstalled(version) { return !version || version === "not installed"; } function shouldShowInstallAfterCheck(component) { const status = getVisibleCheckResult(component)?.status; return status === "outdated" || status === "dev" || status === "outdated_same_release"; } function getVisibleCheckResult(component) { if (!shouldExposeCheckResults({ mounted: updatesMounted, cacheResolved: componentUpdateCheckCacheResolved })) { return null; } return store.get().updatesChecks[component]; } function getLatestVersion(component) { const checkResult = getVisibleCheckResult(component); if (!checkResult || !shouldShowInstallAfterCheck(component)) { return void 0; } return checkResult.latest_version || void 0; } function getGitHubReleaseUrl(component) { const checkResult = getVisibleCheckResult(component); if (!checkResult || !shouldShowInstallAfterCheck(component) || !checkResult.release_url) { return void 0; } return checkResult.release_url; } function isAnyActionLoading() { return Object.values(store.get().updatesActions).some((item) => item.loading); } function isSystemInfoLoading() { const systemInfo = store.get().diagnosticsSystemInfo; return systemInfo.loading || !systemInfo.loaded; } function setActionLoading(action, loading2, local = false) { if (local || !loading2) { setLocalComponentAction(action, loading2 && local); } const updatesActions = store.get().updatesActions; store.set({ updatesActions: { ...updatesActions, [action]: { loading: loading2 } } }); } function beginComponentAction(button) { if (isAnyActionLoading()) { return false; } setActionLoading(button.key, true, true); return true; } function setCheckResult(component, status, latestVersion, releaseUrl = "", currentSha = "", latestSha = "") { const updatesChecks = store.get().updatesChecks; store.set({ updatesChecks: { ...updatesChecks, [component]: { status, latest_version: latestVersion, release_url: releaseUrl, ...currentSha ? { current_sha: currentSha } : {}, ...latestSha ? { latest_sha: latestSha } : {} } } }); } function resetCheckResult(component) { setCheckResult(component, null, ""); } function applyCachedCheckResults(results) { results.forEach((result) => { const status = result.status || null; if (status === "latest" || status === "outdated" || status === "dev" || status === "outdated_same_release") { setCheckResult( result.component, status, result.latest_version || "", result.release_url || "", result.current_sha || "", result.latest_sha || "" ); } }); } function loadComponentUpdateCheckCache({ force = false } = {}) { if (!force && componentUpdateCheckCacheSnapshot) { return Promise.resolve(componentUpdateCheckCacheSnapshot); } if (componentUpdateCheckCachePromise) { return componentUpdateCheckCachePromise; } const promise = TachyonShellMethods.componentUpdateCheckCache().then( (response) => response.success ? response.data : { enabled: false, results: [] } ).then((cache) => { componentUpdateCheckCacheSnapshot = cache; return cache; }).finally(() => { if (componentUpdateCheckCachePromise === promise) { componentUpdateCheckCachePromise = null; } }); componentUpdateCheckCachePromise = promise; return promise; } function getErrorMessage(error, fallback) { return error instanceof Error && error.message ? error.message : fallback; } async function ackComponentActionJob(jobId) { try { const response = await TachyonShellMethods.uiActionAck("component", jobId); if (!response.success) { logger.debug("[UPDATES]", "component action ack failed", response.error); } } catch (error) { logger.debug("[UPDATES]", "component action ack failed", error); } } function getExpectedLatestVersionForAction(button) { if (button.component !== "tachyon" || button.action !== "install" && button.action !== "reinstall") { return void 0; } return store.get().updatesChecks[button.component].latest_version || void 0; } function getCheckToastMessage(status) { if (status === "outdated" || status === "outdated_same_release") { return _("Update is available"); } if (status === "dev") { return _("Installed version is newer than release"); } return _("Latest version is installed"); } async function refreshSystemInfoAfterMutation() { await ensureSystemInfo({ force: true, silent: true }); } function notifyActionProvidersAvailabilityChanged(systemInfo) { if (typeof window === "undefined" || typeof CustomEvent === "undefined") { return; } window.dispatchEvent( new CustomEvent(TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT, { detail: { zapretInstalled: Boolean(systemInfo.zapret_installed), zapret2Installed: Boolean(systemInfo.zapret2_installed), byedpiInstalled: Boolean(systemInfo.byedpi_installed) } }) ); } function reloadPageAfterTachyonUpdate() { window.setTimeout(() => { window.location.reload(); }, 1200); } function patchSystemInfoAfterMutation(result) { const systemInfo = store.get().diagnosticsSystemInfo; const nextSystemInfo = { ...systemInfo, loading: false, loaded: true }; const version = result.current_version || result.latest_version || _("unknown"); if (result.component === "tachyon" && (result.action === "install" || result.action === "reinstall")) { nextSystemInfo.tachyon_version = version; } if (result.component === "sing_box") { nextSystemInfo.sing_box_version = version; if (result.action === "install_extended") { nextSystemInfo.sing_box_extended = 1; nextSystemInfo.sing_box_tiny = 0; nextSystemInfo.sing_box_compressed = 0; nextSystemInfo.sing_box_lx = 0; nextSystemInfo.sing_box_tailscale = 1; } if (result.action === "install_extended_compressed") { nextSystemInfo.sing_box_extended = 1; nextSystemInfo.sing_box_tiny = 0; nextSystemInfo.sing_box_compressed = 1; nextSystemInfo.sing_box_lx = 0; nextSystemInfo.sing_box_tailscale = 1; } if (result.action === "install_lx") { nextSystemInfo.sing_box_extended = 1; nextSystemInfo.sing_box_tiny = 0; nextSystemInfo.sing_box_compressed = 0; nextSystemInfo.sing_box_lx = 1; nextSystemInfo.sing_box_tailscale = 1; } if (result.action === "install_stable") { nextSystemInfo.sing_box_extended = 0; nextSystemInfo.sing_box_tiny = 0; nextSystemInfo.sing_box_compressed = 0; nextSystemInfo.sing_box_lx = 0; nextSystemInfo.sing_box_tailscale = 1; } if (result.action === "install_tiny") { nextSystemInfo.sing_box_extended = 0; nextSystemInfo.sing_box_tiny = 1; nextSystemInfo.sing_box_compressed = 0; nextSystemInfo.sing_box_lx = 0; nextSystemInfo.sing_box_tailscale = 0; } } if (result.component === "zapret") { nextSystemInfo.providerInfoLoaded = true; if (result.action === "remove") { nextSystemInfo.zapret_installed = 0; nextSystemInfo.zapret_version = "not installed"; } else { nextSystemInfo.zapret_installed = 1; nextSystemInfo.zapret_version = version; } } if (result.component === "zapret2") { nextSystemInfo.providerInfoLoaded = true; if (result.action === "remove") { nextSystemInfo.zapret2_installed = 0; nextSystemInfo.zapret2_version = "not installed"; } else { nextSystemInfo.zapret2_installed = 1; nextSystemInfo.zapret2_version = version; } } if (result.component === "byedpi") { nextSystemInfo.providerInfoLoaded = true; if (result.action === "remove") { nextSystemInfo.byedpi_installed = 0; nextSystemInfo.byedpi_version = "not installed"; } else { nextSystemInfo.byedpi_installed = 1; nextSystemInfo.byedpi_version = version; } } const normalizedSystemInfo = normalizeSingBoxVariantFields(nextSystemInfo); store.set({ diagnosticsSystemInfo: normalizedSystemInfo }); if (result.component === "zapret" || result.component === "zapret2" || result.component === "byedpi") { notifyActionProvidersAvailabilityChanged(normalizedSystemInfo); } } async function applyCompletedComponentAction({ key, result, notify }) { const modalController = getActiveProgressModalController(); if (result.action === "check_update") { setActionLoading(key, false); if (!shouldApplyCompletedComponentActionResult(result, notify)) { modalController?.completeSuccess(_("Check completed!")); return; } if (shouldPreserveCompletedCheckResultOnNextMount({ action: result.action, mounted: updatesMounted })) { preserveCheckResultsOnNextMount = true; } const status = result.status || null; const hasUpdate = status === "outdated" || status === "outdated_same_release"; if (status === "latest" || status === "outdated" || status === "dev" || status === "outdated_same_release") { setCheckResult( result.component, status, result.latest_version || "", result.release_url || "", result.current_sha || "", result.latest_sha || "" ); modalController?.updateVersions({ currentVersion: getComponentCurrentVersion(result.component), targetVersion: result.latest_version || "", currentSha: result.current_sha || "", targetSha: result.latest_sha || "" }); } if (notify) { showToast(getCheckToastMessage(status), "success"); } if (hasUpdate) { const installButton = getComponentInstallAction(result.component); modalController?.completeSuccess(getCheckToastMessage(status), { autoCloseMs: 0, installText: installButton.text, onInstall: () => { void handleComponentAction(installButton); } }); } else { modalController?.completeSuccess(getCheckToastMessage(status)); } return; } if (result.action === "install" || result.action === "reinstall" || result.action.startsWith("install_")) { setCheckResult(result.component, "latest", result.latest_version || ""); } else { resetCheckResult(result.component); } patchSystemInfoAfterMutation(result); setActionLoading(key, false); if (result.component === "tachyon" && (result.action === "install" || result.action === "reinstall")) { if (notify && result.message) { showToast(result.message, "success", 1200); } if (notify) { modalController?.completeSuccess(result.message, { reloadPage: true }); reloadPageAfterTachyonUpdate(); } else { modalController?.completeSuccess(result.message); } return; } if (notify && result.message) { showToast(result.message, "success"); } modalController?.completeSuccess(result.message); void refreshSystemInfoAfterMutation(); } async function completeComponentActionJob(key, jobId, response) { if (pageUnloading2) { setActionLoading(key, false); return; } const alreadyHandled = handledComponentJobs.has(jobId); if (alreadyHandled) { setActionLoading(key, false); return; } const shouldNotify = shouldNotifyOwnedUiAction("component", jobId); const modalController = getActiveProgressModalController(); if (!response.success || response.data.success === false) { const message = response.success ? response.data.message || _("Failed to execute") : response.error || _("Failed to execute"); if (isTransientRpcError(message)) { setActionLoading(key, false); void refreshComponentActionState(); return; } handledComponentJobs.add(jobId); setActionLoading(key, false); if (shouldNotify) { showToast(message, "error"); } modalController?.completeError(message); await ackComponentActionJob(jobId); return; } handledComponentJobs.add(jobId); await ackComponentActionJob(jobId); await applyCompletedComponentAction({ key, result: response.data, notify: shouldNotify }); } async function followComponentActionState(state) { const jobId = state.job_id; const key = getComponentActionKey(state.component, state.action); if (!jobId || !key || followedComponentJobs.has(jobId) || handledComponentJobs.has(jobId)) { return; } followedComponentJobs.add(jobId); if (shouldShowLoadingForRestoredAction(state)) { setActionLoading(key, true); if (!getActiveProgressModalController()) { setActiveProgressModalJobId(jobId); showUpdateProgressModal({ component: state.component, action: state.action, componentTitle: getComponentCardTitle(state.component), currentVersion: state.current_version, targetVersion: state.latest_version }); } getActiveProgressModalController()?.startLogTracking(jobId); } try { const response = state.running ? await TachyonShellMethods.waitComponentActionJob( jobId, state.component, state.action, state.latest_version || void 0 ) : { success: true, data: state }; await completeComponentActionJob(key, jobId, response); } catch (error) { logger.error("[UPDATES]", "followComponentActionState failed", error); if (!pageUnloading2) { const message = getErrorMessage(error, _("Failed to execute")); setActionLoading(key, false); if (!isTransientRpcError(message)) { showToast(message, "error"); getActiveProgressModalController()?.completeError(message); } } } finally { followedComponentJobs.delete(jobId); } } async function followAlreadyRunningComponentAction(button) { const uiState = await refreshRuntimeUiState({ force: true }); if (!uiState) { return false; } let state = uiState.actions.component.find( (item) => item.running && item.component === button.component && item.action === button.action && (!item.job_id || !handledComponentJobs.has(item.job_id)) ); if (!state) { state = uiState.actions.component.find( (item) => item.running && item.component === button.component && (!item.job_id || !handledComponentJobs.has(item.job_id)) ); } if (!state) { return false; } if (state.job_id) { markUiActionOwned("component", state.job_id); } await followComponentActionState(state); return true; } function isComponentActionAlreadyRunningError(message) { return Boolean( message && message.includes("Another component action is already running") ); } function handleComponentUiState(uiState) { for (const state of uiState.actions.component || []) { void followComponentActionState(state); } } async function refreshComponentActionState() { if (componentActionStateRefreshPromise) { return componentActionStateRefreshPromise; } componentActionStateRefreshPromise = (async () => { if (!updatesMounted) { return; } const state = await refreshRuntimeUiState({ force: true }); if (!state) { return; } handleComponentUiState(state); })().finally(() => { componentActionStateRefreshPromise = null; }); return componentActionStateRefreshPromise; } function startComponentActionStateWatcher() { if (componentActionStateUnsubscribe) { return; } componentActionStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (updatesMounted) { handleComponentUiState(uiState); } }); } function stopComponentActionStateWatcher() { if (!componentActionStateUnsubscribe) { return; } componentActionStateUnsubscribe(); componentActionStateUnsubscribe = null; } async function handleComponentAction(button) { if (!beginComponentAction(button)) { return; } const cardTitle = getComponentCardTitle(button.component); const currentVersion = getComponentCurrentVersion(button.component); const targetVersion = getExpectedLatestVersionForAction(button); const checkResult = getVisibleCheckResult(button.component); const currentSha = checkResult?.current_sha; const targetSha = checkResult?.latest_sha; const modalController = showUpdateProgressModal({ component: button.component, action: button.action, componentTitle: cardTitle, currentVersion, targetVersion, currentSha, targetSha }); let jobId = ""; let ownsJobFollow = false; try { const startResponse = await TachyonShellMethods.componentActionStart( button.component, button.action ); if (!startResponse.success) { if (isComponentActionAlreadyRunningError(startResponse.error)) { if (await followAlreadyRunningComponentAction(button)) { return; } await new Promise((resolve) => setTimeout(resolve, 800)); if (await followAlreadyRunningComponentAction(button)) { return; } setActionLoading(button.key, false); modalController.completeError( _("Another component action is already running") ); await refreshComponentActionState(); return; } if (isTransientRpcError(startResponse.error)) { if (await followAlreadyRunningComponentAction(button)) { return; } await new Promise((resolve) => setTimeout(resolve, 800)); if (await followAlreadyRunningComponentAction(button)) { return; } setActionLoading(button.key, false); modalController.completeError( startResponse.error || _("Transient connection error") ); await refreshComponentActionState(); return; } throw new Error(startResponse.error); } jobId = startResponse.data.job_id; setActiveProgressModalJobId(jobId); markUiActionOwned("component", jobId); modalController.startLogTracking(jobId); if (followedComponentJobs.has(jobId)) { return; } followedComponentJobs.add(jobId); ownsJobFollow = true; const response = await TachyonShellMethods.waitComponentActionJob( jobId, button.component, button.action, getExpectedLatestVersionForAction(button) ); await completeComponentActionJob(button.key, jobId, response); } catch (error) { logger.error("[UPDATES]", "handleComponentAction failed", error); if (!pageUnloading2) { const message = getErrorMessage(error, _("Failed to execute")); setActionLoading(button.key, false); if (!isTransientRpcError(message)) { showToast(message, "error"); } modalController.completeError(message); void refreshComponentActionState(); } } finally { if (ownsJobFollow) { followedComponentJobs.delete(jobId); } } } function getCheckAction(component, key) { return { key, text: _("Check update"), icon: renderSearchIcon24, component, action: "check_update" }; } function getInstallAction(component, key, installed) { return { key, text: installed ? _("Update") : _("Install"), icon: installed ? renderRotateCcwIcon24 : renderDownloadIcon24, component, action: "install" }; } function getComponentInstallKey(component) { switch (component) { case "tachyon": return "tachyonInstall"; case "sing_box": return "singBoxInstall"; case "zapret": return "zapretInstall"; case "zapret2": return "zapret2Install"; case "byedpi": return "byedpiInstall"; } } function getComponentInstallAction(component) { const isInstalled = !isNotInstalled(getComponentCurrentVersion(component)); const key = getComponentInstallKey(component); return getInstallAction(component, key, isInstalled); } function getInstalledUpdateActions(component, checkKey, installKey, installed = true) { if (!installed) { return []; } const actions = [getCheckAction(component, checkKey)]; if (shouldShowInstallAfterCheck(component)) { actions.push(getInstallAction(component, installKey, true)); } return actions; } function getOptionalComponentActions({ component, installed, checkKey, installKey, removeKey }) { if (!installed) { return [getInstallAction(component, installKey, false)]; } return [ ...getInstalledUpdateActions(component, checkKey, installKey), { key: removeKey, text: _("Remove"), icon: renderXIcon24, component, action: "remove" } ]; } var COMPONENT_REPO_URLS = { tachyon: "https://github.com/Dushnilin/tachyon", sing_box: "https://github.com/SagerNet/sing-box", zapret: "https://github.com/remittor/zapret-openwrt", zapret2: "https://github.com/Dushnilin/zapret2-openwrt", byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt" }; function getComponentCards() { const systemInfo = normalizeSingBoxVariantFields( store.get().diagnosticsSystemInfo ); const systemInfoLoading = isSystemInfoLoading(); const zapretInstalled = Boolean(systemInfo.zapret_installed); const zapret2Installed = Boolean(systemInfo.zapret2_installed); const byedpiInstalled = Boolean(systemInfo.byedpi_installed); const singBoxInstalled = !isNotInstalled(systemInfo.sing_box_version); const singBoxStable = singBoxInstalled && !systemInfo.sing_box_extended && !systemInfo.sing_box_tiny; const singBoxExtended = Boolean(systemInfo.sing_box_extended) && !systemInfo.sing_box_compressed && !systemInfo.sing_box_lx; const singBoxExtendedCompressed = Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_compressed); const singBoxLx = Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_lx); const singBoxTiny = Boolean(systemInfo.sing_box_tiny); const tachyonActions = [ ...getInstalledUpdateActions("tachyon", "tachyonCheck", "tachyonInstall"), { key: "tachyonReinstall", text: _("Reinstall"), icon: renderRotateCcwIcon24, component: "tachyon", action: "reinstall" } ]; const singBoxActions = getInstalledUpdateActions( "sing_box", "singBoxCheck", "singBoxInstall", singBoxInstalled ); if (!singBoxStable) { singBoxActions.push({ key: "singBoxInstallStable", text: "Stable", icon: renderDownloadIcon24, component: "sing_box", action: "install_stable" }); } if (!singBoxTiny) { singBoxActions.push({ key: "singBoxInstallTiny", text: "Tiny", icon: renderDownloadIcon24, component: "sing_box", action: "install_tiny" }); } if (!singBoxExtended) { singBoxActions.push({ key: "singBoxInstallExtended", text: "Extended", icon: renderDownloadIcon24, component: "sing_box", action: "install_extended" }); } if (!singBoxExtendedCompressed) { singBoxActions.push({ key: "singBoxInstallExtendedCompressed", text: "Extended compressed", icon: renderDownloadIcon24, component: "sing_box", action: "install_extended_compressed" }); } if (!singBoxLx) { singBoxActions.push({ key: "singBoxInstallLx", text: "Leadaxe (lx)", icon: renderDownloadIcon24, component: "sing_box", action: "install_lx" }); } const zapretActions = getOptionalComponentActions({ component: "zapret", installed: zapretInstalled, checkKey: "zapretCheck", installKey: "zapretInstall", removeKey: "zapretRemove" }); const zapret2Actions = getOptionalComponentActions({ component: "zapret2", installed: zapret2Installed, checkKey: "zapret2Check", installKey: "zapret2Install", removeKey: "zapret2Remove" }); const byedpiActions = getOptionalComponentActions({ component: "byedpi", installed: byedpiInstalled, checkKey: "byedpiCheck", installKey: "byedpiInstall", removeKey: "byedpiRemove" }); return [ { component: "tachyon", column: 0, title: "Tachyon", version: systemInfoLoading ? _("Loading...") : normalizeCompiledVersion( systemInfo.tachyon_version, systemInfo.tachyon_commit_sha ), latestVersion: getLatestVersion("tachyon"), releaseUrl: getGitHubReleaseUrl("tachyon"), repoUrl: COMPONENT_REPO_URLS.tachyon, actions: tachyonActions }, { component: "sing_box", column: 0, title: "Sing-box", version: systemInfoLoading ? _("Loading...") : formatSingBoxVersion(systemInfo), latestVersion: getLatestVersion("sing_box"), releaseUrl: getGitHubReleaseUrl("sing_box"), repoUrl: systemInfo.sing_box_repo_url || (singBoxLx ? "https://github.com/Leadaxe/sing-box-lx" : singBoxExtended || singBoxExtendedCompressed ? "https://github.com/shtorm-7/sing-box-extended" : COMPONENT_REPO_URLS.sing_box), actions: singBoxActions }, { component: "zapret", column: 1, title: "Zapret", version: systemInfoLoading ? _("Loading...") : zapretInstalled ? systemInfo.zapret_version : _("Not installed"), latestVersion: getLatestVersion("zapret"), releaseUrl: getGitHubReleaseUrl("zapret"), repoUrl: COMPONENT_REPO_URLS.zapret, actions: zapretActions }, { component: "zapret2", column: 1, title: "Zapret2", version: systemInfoLoading ? _("Loading...") : zapret2Installed ? systemInfo.zapret2_version : _("Not installed"), latestVersion: getLatestVersion("zapret2"), releaseUrl: getGitHubReleaseUrl("zapret2"), repoUrl: COMPONENT_REPO_URLS.zapret2, actions: zapret2Actions }, { component: "byedpi", column: 1, title: "ByeDPI", version: systemInfoLoading ? _("Loading...") : byedpiInstalled ? systemInfo.byedpi_version : _("Not installed"), latestVersion: getLatestVersion("byedpi"), releaseUrl: getGitHubReleaseUrl("byedpi"), repoUrl: COMPONENT_REPO_URLS.byedpi, actions: byedpiActions } ]; } function renderComponentCard(card) { const updatesActions = store.get().updatesActions; const anyActionLoading = isAnyActionLoading(); const systemInfoLoading = isSystemInfoLoading(); const checkResult = getVisibleCheckResult(card.component); const headerChildren = [ E("b", { class: "tachyon_updates-page__component__title" }, card.title), E( "span", { class: "tachyon_updates-page__component__header-version" }, card.version ) ]; if (card.repoUrl) { headerChildren.push( E( "a", { class: "tachyon_updates-page__component__repo-link", href: card.repoUrl, target: "_blank", rel: "noopener noreferrer", title: card.repoUrl }, renderGlobeIcon24() ) ); } const header = E( "div", { class: "tachyon_updates-page__component__header" }, headerChildren ); const detailsChildren = []; if (checkResult && checkResult.status) { let labelText = ""; const latestValueNodes = []; if (checkResult.status === "outdated") { labelText = _("Update is available:"); const versionToShow = checkResult.latest_version || card.latestVersion || card.version; if (checkResult.release_url) { latestValueNodes.push( E( "a", { class: "tachyon_updates-page__component__release-version-link", href: checkResult.release_url, target: "_blank", rel: "noopener noreferrer" }, versionToShow || _("Open") ) ); } else if (versionToShow) { latestValueNodes.push(document.createTextNode(versionToShow)); } } else if (checkResult.status === "outdated_same_release") { labelText = _("Update is available for current release"); const build = describeSameReleaseBuild({ currentSha: checkResult.current_sha, latestSha: checkResult.latest_sha }); latestValueNodes.push( E( "span", { class: "tachyon_updates-page__component__sha-info", title: _("Installed build \u2192 Available build") }, build.kind === "sha" ? build.text : _("Rebuilt release") ) ); } else if (checkResult.status === "latest") { labelText = _("Latest version is installed"); } else if (checkResult.status === "dev") { labelText = `${_("Installed version is newer than release")}. ${_("Latest version:")}`; const versionToShow = checkResult.latest_version || card.latestVersion; if (checkResult.release_url) { latestValueNodes.push( E( "a", { class: "tachyon_updates-page__component__release-version-link", href: checkResult.release_url, target: "_blank", rel: "noopener noreferrer" }, versionToShow || _("Open") ) ); } else if (versionToShow) { latestValueNodes.push(document.createTextNode(versionToShow)); } } if (labelText) { const rowChildren = [ E( "span", { class: "tachyon_updates-page__component__info-label" }, labelText ) ]; if (latestValueNodes.length > 0) { rowChildren.push( E( "span", { class: "tachyon_updates-page__component__info-value tachyon_updates-page__component__info-value--latest" }, latestValueNodes ) ); } detailsChildren.push( E( "div", { class: "tachyon_updates-page__component__info-row" }, rowChildren ) ); } } const detailsContainer = detailsChildren.length > 0 ? E( "div", { class: "tachyon_updates-page__component__details" }, detailsChildren ) : null; const primaryActions = []; const dangerActions = []; const variantActions = []; card.actions.forEach((action) => { if (action.action === "remove") { dangerActions.push(action); } else if (action.action.startsWith("install_")) { variantActions.push(action); } else { primaryActions.push(action); } }); const actionElements = []; const primaryButtons = primaryActions.map((action) => { const loading2 = updatesActions[action.key].loading; const isUpdateOrInstall = action.action === "install" || action.action === "reinstall"; return renderButton({ classNames: isUpdateOrInstall ? ["cbi-button-save"] : [], text: action.text, icon: action.icon, loading: loading2, disabled: systemInfoLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }); const dangerButtons = dangerActions.map((action) => { const loading2 = updatesActions[action.key].loading; return renderButton({ classNames: ["cbi-button-remove"], text: action.text, icon: action.icon, loading: loading2, disabled: systemInfoLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }); if (primaryButtons.length > 0 || dangerButtons.length > 0) { actionElements.push( E("div", { class: "tachyon_updates-page__component__actions-main" }, [ ...primaryButtons, ...dangerButtons ]) ); } if (variantActions.length > 0) { const variantButtons = variantActions.map((action) => { const loading2 = updatesActions[action.key].loading; return renderButton({ text: action.text, icon: action.icon, loading: loading2, disabled: systemInfoLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }); actionElements.push( E("div", { class: "tachyon_updates-page__component__variants" }, [ E( "div", { class: "tachyon_updates-page__component__variants-title" }, _("Install another build:") ), E( "div", { class: "tachyon_updates-page__component__variants-buttons" }, variantButtons ) ]) ); } const actionsContainer = E( "div", { class: [ "tachyon_updates-page__component__actions", detailsContainer ? "tachyon_updates-page__component__actions--with-details" : "" ].filter(Boolean).join(" ") }, actionElements ); const cardChildren = [header]; if (detailsContainer) { cardChildren.push(detailsContainer); } cardChildren.push(actionsContainer); return E("div", { class: "tachyon_updates-page__component" }, cardChildren); } function renderUpdatesComponents() { const container = document.getElementById("tachyon_updates-components"); if (!container) { return; } const columns = [[], []]; getComponentCards().forEach((card) => { columns[card.column].push(renderComponentCard(card)); }); return preserveScrollForPage(() => { container.replaceChildren( E( "div", { class: "tachyon_updates-page__components-column" }, columns[0] ), E( "div", { class: "tachyon_updates-page__components-column" }, columns[1] ) ); }); } function onStoreUpdate3(_next, _prev, diff) { if (diff.diagnosticsSystemInfo || diff.updatesActions || diff.updatesChecks || diff.diagnosticsActions || diff.servicesInfoWidget) { renderUpdatesComponents(); } } function applyComponentUpdateCheckCache(componentUpdateCheckCache) { componentUpdateCheckCacheResolved = true; if (componentUpdateCheckCache.enabled) { store.reset(["updatesChecks"]); applyCachedCheckResults(componentUpdateCheckCache.results); } if (shouldResetCheckResultsOnMount({ anyActionLoading: isAnyActionLoading(), preserveCheckResultsOnNextMount, persistentCacheEnabled: componentUpdateCheckCache.enabled })) { store.reset(["updatesChecks"]); } } async function onPageMount4() { onPageUnmount4(); updatesMounted = true; updatesMountId += 1; const mountId = updatesMountId; const cachedRuntimeState = getCachedRuntimeUiState(); const hasRuntimeSnapshot = Boolean(cachedRuntimeState); const needsFreshStateBeforeRender = shouldRefreshComponentStateBeforeRender(cachedRuntimeState); const runtimeStateRefreshPromise = !hasRuntimeSnapshot || needsFreshStateBeforeRender ? refreshRuntimeUiState({ force: true }) : null; const prefetchedComponentUpdateCheckCache = componentUpdateCheckCacheSnapshot; if (prefetchedComponentUpdateCheckCache) { applyComponentUpdateCheckCache(prefetchedComponentUpdateCheckCache); } renderUpdatesComponents(); const componentUpdateCheckCache = await loadComponentUpdateCheckCache({ force: Boolean(prefetchedComponentUpdateCheckCache) }); if (!updatesMounted || mountId !== updatesMountId) { return; } applyComponentUpdateCheckCache(componentUpdateCheckCache); preserveCheckResultsOnNextMount = false; renderUpdatesComponents(); if (runtimeStateRefreshPromise) { await runtimeStateRefreshPromise; if (!updatesMounted || mountId !== updatesMountId) { return; } } store.subscribe(onStoreUpdate3); startComponentActionStateWatcher(); renderUpdatesComponents(); void ensureSystemInfo(); if (hasRuntimeSnapshot) { void refreshRuntimeUiState({ force: true }); } } function onPageUnmount4() { updatesMounted = false; updatesMountId += 1; stopComponentActionStateWatcher(); store.unsubscribe(onStoreUpdate3); } function registerLifecycleListeners4() { if (updatesLifecycleRegistered) { return; } updatesLifecycleRegistered = true; store.subscribe((next, prev, diff) => { if (diff.tabService && next.tabService.current !== prev.tabService.current) { const isUpdatesVisible = next.tabService.current === "updates"; if (isUpdatesVisible) { return onPageMount4(); } if (updatesMounted) { return onPageUnmount4(); } } }); } async function initController4() { if (updatesControllerInitialized) { return; } updatesControllerInitialized = true; void loadComponentUpdateCheckCache(); onMount("updates-status").then(() => { logger.debug("[UPDATES]", "initController", "onMount"); registerLifecycleListeners4(); if (store.get().tabService.current === "updates" || isActiveLuciTab("updates")) { onPageMount4(); } }); } // src/tachyon/tabs/updates/styles.ts var styles6 = ` #cbi-${TACHYON_UCI_PACKAGE}-updates-_mount_node > div { width: 100%; } #cbi-${TACHYON_UCI_PACKAGE}-updates > h3 { display: none; } .tachyon_updates-page { width: 100%; } .tachyon_updates-page__components { display: flex; align-items: flex-start; gap: 10px; width: 100%; flex-wrap: wrap; } .tachyon_updates-page__components-column { display: flex; flex: 1 1 auto; flex-direction: column; gap: 10px; min-width: max-content; } @media (max-width: 760px) { .tachyon_updates-page__components { flex-direction: column; } .tachyon_updates-page__components-column { width: 100%; min-width: 0; } } .tachyon_updates-page__component { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; gap: 10px; min-width: max-content; } .tachyon_updates-page__component__header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; border-bottom: 1px var(--background-color-low, lightgray) solid; padding-bottom: 8px; margin-bottom: 2px; } .tachyon_updates-page__component__title { color: var(--text-color-high); font-size: 16px; font-weight: bold; line-height: 1.2; } .tachyon_updates-page__component__header-version { color: var(--text-color-medium, #888); font-size: 13px; font-weight: normal; } .tachyon_updates-page__component__repo-link { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 3px; color: var(--text-color-medium, #888); text-decoration: none; transition: color 0.15s, background-color 0.15s; flex-shrink: 0; } .tachyon_updates-page__component__repo-link:hover { color: var(--link-color, #3498db); background-color: var(--background-color-low, rgba(0, 0, 0, 0.05)); } .tachyon_updates-page__component__details { display: flex; flex-direction: column; gap: 6px; } .tachyon_updates-page__component__info-row { display: flex; justify-content: flex-start; align-items: center; min-height: 24px; gap: 8px; white-space: nowrap; } .tachyon_updates-page__component__info-label { color: var(--text-color-medium, #888); font-size: 12px; } .tachyon_updates-page__component__info-value { color: var(--text-color-high, #000); font-weight: 500; font-size: 13px; text-align: left; display: flex; align-items: center; gap: 6px; min-width: 0; overflow-wrap: anywhere; } .tachyon_updates-page__component__info-value--latest { flex-wrap: wrap; justify-content: flex-start; } .tachyon_updates-page__component__release-version-link { color: var(--link-color, #3498db) !important; text-decoration: underline; font-weight: bold; } .tachyon_updates-page__component__release-version-link:hover { color: var(--link-color-dark, #2980b9) !important; } .tachyon_updates-page__component__sha-info { font-family: monospace; font-size: 11px; color: var(--text-color-medium, #888); background: var(--background-color-low, rgba(0,0,0,0.06)); border-radius: 3px; padding: 1px 5px; letter-spacing: 0.02em; } .tachyon_updates-page__component__actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; } .tachyon_updates-page__component__actions--with-details { border-top: 1px var(--background-color-low, lightgray) solid; padding-top: 10px; } .tachyon_updates-page__component__actions-main { display: flex; justify-content: flex-start; align-items: center; flex-wrap: nowrap; gap: 6px; } .tachyon_updates-page__component__variants { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; } .tachyon_updates-page__component__variants-title { font-size: 11px; font-weight: bold; color: var(--text-color-medium, gray); } .tachyon_updates-page__component__variants-buttons { display: flex; flex-wrap: nowrap; gap: 6px; } .tachyon-update-modal__body { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 640px; box-sizing: border-box; padding: 6px 0; } .tachyon-update-modal__header { display: flex; justify-content: space-between; align-items: center; gap: 10px; border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.12)); padding-bottom: 10px; } .tachyon-update-modal__header-info { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .tachyon-update-modal__component-name { font-size: 16px; color: var(--text-color-high, #fff); } .tachyon-update-modal__version-badge { font-size: 12px; font-family: monospace; padding: 2px 6px; border-radius: 4px; background: var(--background-color-low, rgba(0,0,0,0.1)); color: var(--text-color-medium, #aaa); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); } .tachyon-update-modal__timer-badge { font-size: 13px; font-family: monospace; color: var(--text-color-medium, #888); } .tachyon-update-modal__success-banner { display: flex; align-items: center; gap: 8px; color: #2ecc71; font-weight: bold; } .tachyon-update-modal__error-banner { display: flex; align-items: center; gap: 8px; color: #e74c3c; font-weight: bold; } .tachyon-update-modal__actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-top: 6px; border-top: 1px solid var(--border-color, rgba(255,255,255,0.12)); padding-top: 10px; } .tachyon-update-modal__log-panel { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-height: 0; } .tachyon-update-modal__log-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; } .tachyon-update-modal__log-copy { padding: 2px 8px; font-size: 12px; } .tachyon-update-modal__log { margin: 0; height: 350px; min-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-family: monospace; font-size: 11px; line-height: 1.5; color: var(--text-color, #ddd); background: rgba(0, 0, 0, 0.35); border: 1px solid var(--border-color, rgba(255,255,255,0.12)); border-radius: 4px; padding: 8px; flex: 1 1 auto; min-height: 0; } `; // src/tachyon/tabs/updates/index.ts var UpdatesTab = { render: render4, initController: initController4, styles: styles6 }; // src/styles.ts var GlobalStyles = ` ${DashboardTab.styles} ${DiagnosticTab.styles} ${MonitoringTab.styles} ${UpdatesTab.styles} ${PartialStyles} /* Hide extra H3 for settings tab */ #cbi-${TACHYON_UCI_PACKAGE}-settings > h3 { display: none; } /* Hide extra H3 for rules tab */ #cbi-${TACHYON_UCI_PACKAGE}-section > h3:nth-child(1) { display: none; } /* Vertical align for remove rule action button */ #cbi-${TACHYON_UCI_PACKAGE}-section > .cbi-section-remove { margin-bottom: -32px; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-actions > div { display: inline-flex; align-items: center; gap: 4px; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-actions { text-align: right; } /* Rule reorder visuals */ #cbi-${TACHYON_UCI_PACKAGE}-section { position: relative; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row { position: relative; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.placeholder { opacity: 1; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.placeholder em { font-style: italic; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after, #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-below::after { content: ''; position: absolute; left: 10px; right: 10px; height: 2px; border-radius: 2px; background: var(--primary-color-high, #1976d2); pointer-events: none; z-index: 2; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after { top: -1px; } #cbi-${TACHYON_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-below::after { bottom: -1px; } /* Centered class helper */ .centered { display: flex; align-items: center; justify-content: center; } /* Rotate class helper */ .rotate { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Skeleton styles*/ .skeleton { background-color: var(--background-color-low, #e0e0e0); border-radius: 4px; position: relative; overflow: hidden; } .skeleton::after { content: ''; position: absolute; top: 0; left: -150%; width: 150%; height: 100%; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.4), transparent ); animation: skeleton-shimmer 1.6s infinite; } @keyframes skeleton-shimmer { 100% { left: 150%; } } /* Toast */ .toast-container { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 10px; z-index: 9999; font-family: system-ui, sans-serif; } .toast { opacity: 0; transform: translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; padding: 10px 16px; border-radius: 6px; color: #fff; font-size: 14px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); min-width: 220px; max-width: 340px; text-align: center; } .toast-success { background-color: #28a745; } .toast-error { background-color: #dc3545; } .toast.visible { opacity: 1; transform: translateY(0); } `; // src/helpers/injectGlobalStyles.ts var TACHYON_GLOBAL_STYLES_ID = "tachyon-global-styles"; function injectGlobalStyles() { if (document.getElementById(TACHYON_GLOBAL_STYLES_ID)) { return; } document.head.insertAdjacentHTML( "beforeend", ` ` ); } // src/main.ts if (typeof structuredClone !== "function") globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj)); return baseclass.extend({ BOOTSTRAP_DNS_SERVER_OPTIONS, DEFAULT_LATENCY_TEST_URL, DNS_SERVERS_BY_PROTOCOL, DOMAIN_LIST_OPTIONS, DashboardTab, DiagnosticTab, LATENCY_TEST_URL_OPTIONS, MonitoringTab, TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT, TACHYON_UCI_PACKAGE, TachyonShellMethods, UpdatesTab, applyUiStateToStore, bulkValidate, coreService, getClashUIUrl, getProxyUrlName, injectGlobalStyles, parseValueList, showToast, store, validateDNS, validateDomain, validateIP, validateOutboundJson, validatePath, validateProxyUrl, validateSubnet, validateUrl });