- getZapret2Status/getZapretStatus/getSingBoxStatus/getByedpiStatus: добавлен allowNonZeroWithStdout — rpcd может вернуть не-zero exit при наличии полезного stdout (модуль-провайдер не найден, но JSON-статус всё равно отдан). Без этого флага callBaseMethod считал вызов провалившимся → serviceStatus undefined → «Неизвестный» - customProxyLatencies: сервисные секции (SING_BOX, ZAPRET, ZAPRET2, BYEDPI) больше не перезаписываются кешем latency-теста. Ранее неудачный тест кешировал -1, перезаписывая latency:0 из секции → отображалось «-1ms» вместо статуса
18278 lines
551 KiB
JavaScript
18278 lines
551 KiB
JavaScript
// 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 canUseDirectClashApi() {
|
|
const loc = typeof window !== "undefined" ? window.location : void 0;
|
|
return Boolean(loc?.hostname && loc.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 && outbound.latency > 0 ? `\u25CF ${outbound.latency} ms` : outbound.latency === -1 ? `\u25CF ${_("Not responding")}` : outbound.runtimeAvailable ? `\u25CF ${_("Connected")}` : `\u25CF ${_("Not connected")}`;
|
|
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)";
|
|
if (selectedOutbound.latency && selectedOutbound.latency > 0) {
|
|
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)";
|
|
}
|
|
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 && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : selectedOutbound.latency === -1 ? _("Not responding") : selectedOutbound.runtimeAvailable ? _("Connected") : _("Not 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" }, [
|
|
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: []
|
|
})
|
|
)
|
|
]),
|
|
E("div", { id: "dashboard-connections-grid" }, []),
|
|
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_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
|
|
);
|
|
}
|
|
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,
|
|
[],
|
|
"/usr/bin/tachyon",
|
|
{ allowNonZeroWithStdout: true }
|
|
),
|
|
getZapretStatus: async () => callBaseMethod(
|
|
Tachyon.AvailableMethods.GET_ZAPRET_STATUS,
|
|
[],
|
|
"/usr/bin/tachyon",
|
|
{ allowNonZeroWithStdout: true }
|
|
),
|
|
getZapret2Status: async () => callBaseMethod(
|
|
Tachyon.AvailableMethods.GET_ZAPRET2_STATUS,
|
|
[],
|
|
"/usr/bin/tachyon",
|
|
{ allowNonZeroWithStdout: true }
|
|
),
|
|
getByedpiStatus: async () => callBaseMethod(
|
|
Tachyon.AvailableMethods.GET_BYEDPI_STATUS,
|
|
[],
|
|
"/usr/bin/tachyon",
|
|
{ allowNonZeroWithStdout: true }
|
|
),
|
|
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;
|
|
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);
|
|
}
|
|
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 (isSelfUpdate) {
|
|
const version = await confirmedByVersion() || await confirmedSameVersionReinstall();
|
|
if (version) {
|
|
if (await settleVersion(version)) {
|
|
return selfUpdateResult(version);
|
|
}
|
|
continue;
|
|
}
|
|
if (!stateResponse?.running) {
|
|
continue;
|
|
}
|
|
transientRpc.reset();
|
|
continue;
|
|
}
|
|
if (stateResponse?.running) {
|
|
transientRpc.reset();
|
|
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) {
|
|
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,
|
|
custom_device_headers: item.custom_device_headers,
|
|
device_os: item.device_os,
|
|
ver_os: item.ver_os,
|
|
device_model: item.device_model,
|
|
device_locale: item.device_locale,
|
|
app_version: item.app_version,
|
|
accept_language: item.accept_language,
|
|
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 == 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: sectionAction === "awg" ? "AmneziaWG" : sectionAction === "warp" ? "WARP" : 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 (next[key] === prev[key]) continue;
|
|
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 RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS = 3e4;
|
|
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() {
|
|
if (!isDocumentVisible()) {
|
|
return RUNTIME_UI_STATE_HIDDEN_POLL_INTERVAL_MS;
|
|
}
|
|
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/helpers/capCollectionSize.ts
|
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
function capSetSize(set, max = DEFAULT_MAX_ENTRIES) {
|
|
while (set.size > max) {
|
|
const oldest = set.values().next().value;
|
|
if (oldest === void 0) {
|
|
return;
|
|
}
|
|
set.delete(oldest);
|
|
}
|
|
}
|
|
function capMapSize(map, max = DEFAULT_MAX_ENTRIES) {
|
|
while (map.size > max) {
|
|
const oldest = map.keys().next().value;
|
|
if (oldest === void 0) {
|
|
return;
|
|
}
|
|
map.delete(oldest);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
capSetSize(handledSubscriptionJobs);
|
|
}
|
|
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);
|
|
capSetSize(handledLatencyJobs);
|
|
}
|
|
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);
|
|
}
|
|
capMapSize(customProxyLatencies);
|
|
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 SERVICE_TYPES = /* @__PURE__ */ new Set(["SING_BOX", "ZAPRET", "ZAPRET2", "BYEDPI"]);
|
|
const sectionsWithCustomLatencies = sectionsWidget.data.map((section) => ({
|
|
...section,
|
|
outbounds: section.outbounds.map((outbound) => ({
|
|
...outbound,
|
|
latency: SERVICE_TYPES.has(outbound.type) || !customProxyLatencies.has(outbound.code) ? outbound.latency : customProxyLatencies.get(outbound.code)
|
|
}))
|
|
}));
|
|
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/helpers/notifyActionFailure.ts
|
|
function extractMessage(response) {
|
|
if (!response || typeof response !== "object") {
|
|
return null;
|
|
}
|
|
const record = response;
|
|
for (const key of ["error", "message", "stderr"]) {
|
|
const value = record[key];
|
|
if (typeof value === "string" && value.trim() !== "") {
|
|
return value.trim();
|
|
}
|
|
}
|
|
const data = record.data;
|
|
if (data && typeof data === "object") {
|
|
const dataError = data.error;
|
|
if (typeof dataError === "string" && dataError.trim() !== "") {
|
|
return dataError.trim();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function notifyActionFailure(context, response, fallbackLabel) {
|
|
logger.error("[DIAGNOSTIC]", context, response);
|
|
const detail = extractMessage(response);
|
|
showToast(
|
|
(fallbackLabel ?? _("Action failed")) + (detail ? ": " + detail : ""),
|
|
"error"
|
|
);
|
|
}
|
|
|
|
// 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,
|
|
restoreNativeInternet,
|
|
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(!!restoreNativeInternet?.visible, [
|
|
renderButton({
|
|
classNames: ["cbi-button-reset"],
|
|
onClick: restoreNativeInternet.onClick,
|
|
icon: renderCircleStopIcon24,
|
|
text: _("Restore Native Internet"),
|
|
loading: restoreNativeInternet.loading,
|
|
disabled: restoreNativeInternet.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 _("Direct");
|
|
const clean = routeType.trim();
|
|
if (clean === "connection" || clean === "proxy" || clean === "outbound") {
|
|
return _("Proxy");
|
|
}
|
|
if (clean === "direct") {
|
|
return _("Direct");
|
|
}
|
|
if (clean === "auto") {
|
|
return _("Auto");
|
|
}
|
|
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": _("Basic connectivity"),
|
|
"\u0417\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0432 \u0420\u0424": _("Blocked resources"),
|
|
"\u0421\u0432\u043E\u0438 \u0434\u043E\u043C\u0435\u043D\u044B": _("Custom domains"),
|
|
Custom: _("Custom"),
|
|
"ChatGPT / OpenAI": "ChatGPT / OpenAI",
|
|
"Gemini / Google AI": "Gemini / Google AI",
|
|
"X (Twitter)": "Twitter (X)",
|
|
"UDP / QUIC": _("UDP / QUIC protocols"),
|
|
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;"
|
|
},
|
|
_("Pending...")
|
|
);
|
|
}
|
|
if (isTesting) {
|
|
return E(
|
|
"span",
|
|
{
|
|
class: "badge cbi-button-action",
|
|
style: "font-size: 11px; padding: 2px 8px;"
|
|
},
|
|
_("Testing...")
|
|
);
|
|
}
|
|
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;"
|
|
},
|
|
_("Available")
|
|
);
|
|
}
|
|
return E(
|
|
"span",
|
|
{
|
|
class: "badge cbi-button-reset",
|
|
style: "font-weight: bold; font-size: 11px; padding: 2px 8px;"
|
|
},
|
|
cleanStatus || _("Unavailable")
|
|
);
|
|
}
|
|
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;" },
|
|
_("Loading service list...")
|
|
),
|
|
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: _("Close"),
|
|
onClick: () => ui.hideModal()
|
|
})
|
|
]
|
|
)
|
|
]
|
|
);
|
|
ui.showModal(_("Service availability check"), modalContent);
|
|
const loadTargets = (targetMode) => {
|
|
container.innerHTML = "";
|
|
container.appendChild(
|
|
E(
|
|
"p",
|
|
{ style: "text-align: center; margin-top: 20px;" },
|
|
_("Loading service list...")
|
|
)
|
|
);
|
|
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);" },
|
|
_("Failed to start service check.")
|
|
)
|
|
);
|
|
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);" },
|
|
_("Failed to parse target list.")
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
if (targets.length === 0) {
|
|
container.appendChild(
|
|
E("p", {}, _("No check targets found in the configuration."))
|
|
);
|
|
return;
|
|
}
|
|
const activeSectionsBtn = renderButton({
|
|
text: _("Active routes"),
|
|
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: _("All services"),
|
|
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;" },
|
|
_("Check mode:")
|
|
),
|
|
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(_("Total targets"), totalStatEl),
|
|
createStatCard(_("Available"), passedStatEl),
|
|
createStatCard(_("Unavailable"), failedStatEl),
|
|
createStatCard(_("Avg. latency"), 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" },
|
|
`${_("All services")} (${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: _("Search domain or service..."),
|
|
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;" },
|
|
_("Filter by service:")
|
|
),
|
|
sectionSelect
|
|
]
|
|
),
|
|
searchInput
|
|
]
|
|
);
|
|
const tableHead = E("tr", { class: "tr cbi-section-table-titles" }, [
|
|
E(
|
|
"th",
|
|
{ class: "th", style: "width: 28%; padding: 8px;" },
|
|
_("Service / Target")
|
|
),
|
|
E(
|
|
"th",
|
|
{ class: "th", style: "width: 17%; padding: 8px;" },
|
|
_("Route")
|
|
),
|
|
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;"
|
|
},
|
|
_("Status")
|
|
)
|
|
]);
|
|
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: _(
|
|
"Enter a domain or IP to check (e.g. example.com)..."
|
|
),
|
|
class: "cbi-input-text",
|
|
style: "width: 270px; font-size: 12px;"
|
|
});
|
|
const customBtn = renderButton({
|
|
text: _("Check domain"),
|
|
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 = _("Check domain");
|
|
}
|
|
}
|
|
});
|
|
const leftWrap = E(
|
|
"div",
|
|
{
|
|
style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;"
|
|
},
|
|
[customDomainInput, customBtn]
|
|
);
|
|
const checkAllBtn = renderButton({
|
|
text: _("Check all"),
|
|
classNames: ["cbi-button-action"],
|
|
onClick: async () => {
|
|
checkAllBtn.disabled = true;
|
|
checkAllBtn.textContent = _("Testing...");
|
|
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 = _("Check again");
|
|
checkAllBtn.disabled = false;
|
|
}
|
|
});
|
|
const closeBtn = renderButton({
|
|
text: _("Close"),
|
|
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);" },
|
|
_("Failed to start service check.")
|
|
)
|
|
);
|
|
});
|
|
};
|
|
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()
|
|
});
|
|
let sendInFlight = false;
|
|
const handleSend = async (queryText) => {
|
|
const text = (queryText || chatInput.value).trim();
|
|
if (!text || sendInFlight) return;
|
|
sendInFlight = true;
|
|
chatInput.disabled = true;
|
|
sendBtn.disabled = true;
|
|
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,
|
|
[text],
|
|
"/usr/bin/tachyon",
|
|
{ timeout: 12e4 }
|
|
);
|
|
chatHistory.pop();
|
|
let answerText = _("Failed to receive response from AI service.");
|
|
if (res && res.success) {
|
|
const d = res.data;
|
|
if (typeof d === "object" && d !== null) {
|
|
answerText = String(
|
|
d.report ?? d.summary ?? d.message ?? d.raw ?? JSON.stringify(d)
|
|
);
|
|
} else if (typeof d === "string") {
|
|
try {
|
|
const parsed = JSON.parse(d);
|
|
answerText = String(
|
|
parsed.report ?? parsed.summary ?? parsed.message ?? parsed.raw ?? d
|
|
);
|
|
} catch (_e) {
|
|
answerText = d;
|
|
}
|
|
}
|
|
}
|
|
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();
|
|
sendInFlight = false;
|
|
chatInput.disabled = false;
|
|
sendBtn.disabled = false;
|
|
};
|
|
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 ai_doctor_api_key",
|
|
"option tuic_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);
|
|
capSetSize(handledServiceActionJobs);
|
|
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) {
|
|
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);
|
|
capSetSize(handledServiceActionJobs);
|
|
capSetSize(handledServiceActionJobs);
|
|
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 {
|
|
notifyActionFailure(
|
|
"handleShowGlobalCheck",
|
|
globalCheck,
|
|
_("Global check failed")
|
|
);
|
|
}
|
|
} catch (e) {
|
|
notifyActionFailure("handleShowGlobalCheck", e, _("Global check failed"));
|
|
} 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 {
|
|
notifyActionFailure("handleViewLogs", viewLogs, _("View logs failed"));
|
|
}
|
|
} catch (e) {
|
|
notifyActionFailure("handleViewLogs", e, _("View logs failed"));
|
|
} 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;"
|
|
},
|
|
[
|
|
E(
|
|
"div",
|
|
{
|
|
style: "display: flex; gap: 8px; flex-wrap: wrap; align-items: center;"
|
|
},
|
|
[
|
|
renderButton({
|
|
classNames: ["cbi-button-action"],
|
|
text: "\u{1F4CB} " + _("Copy Support Report"),
|
|
onClick: copySupportReport
|
|
}),
|
|
renderButton({
|
|
classNames: ["cbi-button-reset"],
|
|
text: "\u{1F6A8} " + _("Restore Native Internet (Stop Tachyon)"),
|
|
onClick: async () => {
|
|
showToast(
|
|
_(
|
|
"Restoring native direct internet (stopping Tachyon)..."
|
|
),
|
|
"success"
|
|
);
|
|
const fixRes = await TachyonShellMethods.applyQuickFix(
|
|
"restore_native_internet"
|
|
);
|
|
if (fixRes && typeof fixRes === "object" && fixRes.success) {
|
|
showToast(
|
|
_("Native internet restored. Tachyon stopped."),
|
|
"success"
|
|
);
|
|
ui.hideModal();
|
|
await refreshDiagnosticServicesInfo({
|
|
force: true,
|
|
allowInactive: true
|
|
});
|
|
} else {
|
|
showToast(
|
|
_("Failed to restore native internet"),
|
|
"error"
|
|
);
|
|
}
|
|
}
|
|
})
|
|
]
|
|
),
|
|
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();
|
|
}
|
|
function handleRestoreNativeInternet() {
|
|
ui.showModal(
|
|
_("Restore Native Internet"),
|
|
E("div", {}, [
|
|
E(
|
|
"p",
|
|
{},
|
|
_(
|
|
"This action will stop Tachyon, restore standard dnsmasq/DNS, remove all proxy/anti-censorship firewall rules and restore direct Internet connection through your ISP."
|
|
)
|
|
),
|
|
E(
|
|
"div",
|
|
{
|
|
class: "right",
|
|
style: "display: flex; justify-content: flex-end; gap: 8px; margin-top: 15px;"
|
|
},
|
|
[
|
|
renderButton({
|
|
classNames: ["cbi-button-neutral"],
|
|
text: _("Cancel"),
|
|
onClick: () => ui.hideModal()
|
|
}),
|
|
renderButton({
|
|
classNames: ["cbi-button-reset"],
|
|
text: "\u{1F6A8} " + _("Restore Native Internet"),
|
|
onClick: async () => {
|
|
ui.hideModal();
|
|
showToast(
|
|
_("Restoring native direct internet (stopping Tachyon)..."),
|
|
"success"
|
|
);
|
|
const res = await TachyonShellMethods.applyQuickFix(
|
|
"restore_native_internet"
|
|
);
|
|
if (res && typeof res === "object" && res.success) {
|
|
showToast(
|
|
_("Native internet restored. Tachyon stopped."),
|
|
"success"
|
|
);
|
|
await refreshDiagnosticServicesInfo({
|
|
force: true,
|
|
allowInactive: true
|
|
});
|
|
} else {
|
|
showToast(_("Failed to restore native internet"), "error");
|
|
}
|
|
}
|
|
})
|
|
]
|
|
)
|
|
])
|
|
);
|
|
}
|
|
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 {
|
|
notifyActionFailure(
|
|
"handleShowSingBoxConfig",
|
|
showSingBoxConfig,
|
|
_("Show sing-box config failed")
|
|
);
|
|
}
|
|
} catch (e) {
|
|
notifyActionFailure(
|
|
"handleShowSingBoxConfig",
|
|
e,
|
|
_("Show sing-box config failed")
|
|
);
|
|
} 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
|
|
},
|
|
restoreNativeInternet: {
|
|
loading: false,
|
|
visible: true,
|
|
onClick: handleRestoreNativeInternet,
|
|
disabled: false
|
|
},
|
|
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) {
|
|
const raw = normalizeString(localDeviceChoices[ip]);
|
|
if (!raw) return "";
|
|
const match = raw.match(/^(?:IP|MAC):\s*[^\s—]+\s*—\s*(.+)$/i);
|
|
if (match && match[1]) {
|
|
return match[1].trim();
|
|
}
|
|
if (!raw.startsWith("IP:") && !raw.startsWith("MAC:") && raw !== ip) {
|
|
return raw;
|
|
}
|
|
return "";
|
|
}
|
|
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 ? `${deviceName} (${ip})` : 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} (${ip})`,
|
|
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", {}, "-");
|
|
const row = 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])
|
|
]
|
|
);
|
|
row.setAttribute("data-connection-id", connection.id);
|
|
row.setAttribute("data-row-signature", getConnectionRowSignature(connection));
|
|
return row;
|
|
}
|
|
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;
|
|
}
|
|
patchConnectionRows(visibleConnections);
|
|
container.scrollLeft = previousScrollLeft;
|
|
}
|
|
function getConnectionRowSignature(connection) {
|
|
const target = getTargetCellParts(connection);
|
|
const source = getSourceCellParts(connection);
|
|
return [
|
|
target.primary,
|
|
getNetwork(connection),
|
|
getRoute(connection),
|
|
source.primary,
|
|
source.ip,
|
|
closingConnectionIds.has(connection.id) ? "closing" : "open"
|
|
].join("|");
|
|
}
|
|
function patchConnectionRows(visibleConnections) {
|
|
const currentTbody = container_queryConnectionsTbody();
|
|
const hasKeyedRows = currentTbody && currentTbody.querySelector("tr[data-connection-id]") != null;
|
|
if (!currentTbody || !hasKeyedRows) {
|
|
document.getElementById("monitoring-connections")?.replaceChildren(renderConnectionsTable(visibleConnections));
|
|
return;
|
|
}
|
|
const tbody = currentTbody;
|
|
const existingRows = /* @__PURE__ */ new Map();
|
|
for (const row of Array.from(tbody.children)) {
|
|
const id = row.getAttribute("data-connection-id");
|
|
if (id) {
|
|
existingRows.set(id, row);
|
|
}
|
|
}
|
|
const seenIds = /* @__PURE__ */ new Set();
|
|
for (const connection of visibleConnections) {
|
|
seenIds.add(connection.id);
|
|
const signature = getConnectionRowSignature(connection);
|
|
const existing = existingRows.get(connection.id);
|
|
if (!existing) {
|
|
tbody.appendChild(renderConnectionRow(connection));
|
|
continue;
|
|
}
|
|
if (existing.getAttribute("data-row-signature") !== signature) {
|
|
tbody.replaceChild(renderConnectionRow(connection), existing);
|
|
continue;
|
|
}
|
|
patchVolatileCells(existing, connection);
|
|
tbody.appendChild(existing);
|
|
}
|
|
for (const [id, row] of existingRows) {
|
|
if (!seenIds.has(id)) {
|
|
row.remove();
|
|
}
|
|
}
|
|
}
|
|
function container_queryConnectionsTbody() {
|
|
return document.getElementById("monitoring-connections")?.querySelector("tbody") ?? null;
|
|
}
|
|
function patchVolatileCells(row, connection) {
|
|
const cells = row.children;
|
|
if (cells.length < 7) {
|
|
return;
|
|
}
|
|
const volatileByIndex = [
|
|
[
|
|
3,
|
|
() => renderTableCell(_("Time"), [
|
|
renderValue(formatConnectionDuration(connection))
|
|
])
|
|
],
|
|
[
|
|
4,
|
|
() => renderTableCell(_("Downloaded"), [
|
|
renderValue(formatBytes2(connection.download))
|
|
])
|
|
],
|
|
[
|
|
5,
|
|
() => renderTableCell(_("Uploaded"), [
|
|
renderValue(formatBytes2(connection.upload))
|
|
])
|
|
]
|
|
];
|
|
for (const [index, makeCell] of volatileByIndex) {
|
|
const current = cells[index];
|
|
const next = makeCell();
|
|
if (current) {
|
|
row.replaceChild(next, current);
|
|
} else {
|
|
row.appendChild(next);
|
|
}
|
|
}
|
|
}
|
|
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: 25%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(2) {
|
|
width: 5%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(3) {
|
|
width: 9%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(4) {
|
|
width: 7%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(5) {
|
|
width: 9%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(6) {
|
|
width: 8%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(7) {
|
|
width: 33%;
|
|
}
|
|
|
|
.tachyon_monitoring-page__table th:nth-child(8) {
|
|
width: 4%;
|
|
}
|
|
|
|
.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));
|
|
}
|
|
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"
|
|
},
|
|
_("Operation started. Waiting for log output...")
|
|
);
|
|
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 reloadProbeActive = false;
|
|
function appendLogText(text) {
|
|
if (!text) {
|
|
return;
|
|
}
|
|
logFullText += text;
|
|
logPreEl.textContent = logFullText;
|
|
logPreEl.scrollTop = logPreEl.scrollHeight;
|
|
}
|
|
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!") : _("Operation completed successfully!"));
|
|
const bannerEl = E(
|
|
"div",
|
|
{ class: "tachyon-update-modal__success-banner" },
|
|
[renderCheckIcon24(), E("span", {}, successMsg)]
|
|
);
|
|
if (opts?.reloadPage) {
|
|
reloadProbeActive = true;
|
|
let attempt = 0;
|
|
const maxAttempts = 30;
|
|
const statusEl = E(
|
|
"div",
|
|
{
|
|
class: "tachyon-update-modal__reload-status",
|
|
style: "margin: 10px 0; font-size: 0.95em; color: var(--cbi-color-neutral, #666);"
|
|
},
|
|
_("Waiting for router services to restart...")
|
|
);
|
|
const reloadBtn = renderButton({
|
|
classNames: ["cbi-button-save"],
|
|
text: _("Reload now"),
|
|
onClick: () => {
|
|
reloadProbeActive = false;
|
|
window.location.reload();
|
|
}
|
|
});
|
|
actionButtonContainer.replaceChildren(
|
|
bannerEl,
|
|
statusEl,
|
|
E("div", { class: "tachyon-update-modal__action-buttons" }, [
|
|
reloadBtn
|
|
])
|
|
);
|
|
const probeAndReload = async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
while (reloadProbeActive && attempt < maxAttempts) {
|
|
attempt += 1;
|
|
statusEl.textContent = `${_("Verifying router and service readiness...")} (${attempt}/${maxAttempts})`;
|
|
try {
|
|
const res = await TachyonShellMethods.getSystemInfo();
|
|
if (res && res.success) {
|
|
statusEl.textContent = _(
|
|
"Services are online, waiting for full initialization..."
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
if (!reloadProbeActive) return;
|
|
try {
|
|
const confirm = await TachyonShellMethods.getSystemInfo();
|
|
if (confirm && confirm.success) {
|
|
statusEl.textContent = _(
|
|
"Services are online and ready! Reloading page..."
|
|
);
|
|
reloadBtn.textContent = _("Reloading...");
|
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
if (activeModalJobId) {
|
|
try {
|
|
sessionStorage.setItem(
|
|
"tachyon_post_update_job",
|
|
activeModalJobId
|
|
);
|
|
} catch (_e) {
|
|
}
|
|
}
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
} catch (_confirmErr) {
|
|
}
|
|
}
|
|
} catch (_err) {
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
}
|
|
if (reloadProbeActive) {
|
|
statusEl.textContent = _(
|
|
"Services restarted. Click to reload page."
|
|
);
|
|
}
|
|
};
|
|
void probeAndReload();
|
|
} 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(
|
|
bannerEl,
|
|
E(
|
|
"div",
|
|
{ class: "tachyon-update-modal__action-buttons" },
|
|
actionButtons
|
|
)
|
|
);
|
|
const defaultAutoCloseMs = opts?.onInstall ? 0 : isCheckAction ? 1200 : 0;
|
|
const autoCloseMs = opts?.autoCloseMs ?? defaultAutoCloseMs;
|
|
if (autoCloseMs > 0) {
|
|
setTimeout(() => {
|
|
if (activeModalController === controller || activeModalController === null) {
|
|
controller.close();
|
|
}
|
|
}, autoCloseMs);
|
|
}
|
|
}
|
|
},
|
|
completeError: (errorMessage) => {
|
|
cleanupTimers();
|
|
finishLogTracking();
|
|
const bannerEl = 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.replaceChildren(
|
|
bannerEl,
|
|
E("div", { class: "tachyon-update-modal__action-buttons" }, [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();
|
|
reloadProbeActive = false;
|
|
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();
|
|
try {
|
|
const savedJob = sessionStorage.getItem("tachyon_post_update_job");
|
|
if (savedJob) {
|
|
handledComponentJobs.add(savedJob);
|
|
capSetSize(handledComponentJobs);
|
|
followedComponentJobs.add(savedJob);
|
|
sessionStorage.removeItem("tachyon_post_update_job");
|
|
}
|
|
} catch (_e) {
|
|
}
|
|
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)
|
|
}
|
|
})
|
|
);
|
|
}
|
|
var RELOAD_POLL_INTERVAL_MS = 1e3;
|
|
var RELOAD_POLL_MAX_WAIT_MS = 3e4;
|
|
async function waitForTachyonResponsive() {
|
|
const deadline = Date.now() + RELOAD_POLL_MAX_WAIT_MS;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await TachyonShellMethods.getUiState();
|
|
if (response.success) {
|
|
return true;
|
|
}
|
|
} catch {
|
|
}
|
|
await new Promise(
|
|
(resolve) => setTimeout(resolve, RELOAD_POLL_INTERVAL_MS)
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
function reloadPageAfterTachyonUpdate() {
|
|
void waitForTachyonResponsive().finally(() => {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
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) {
|
|
if (modalController) {
|
|
modalController.completeSuccess(result.message, { reloadPage: true });
|
|
} else {
|
|
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) {
|
|
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);
|
|
capSetSize(handledComponentJobs);
|
|
setActionLoading(key, false);
|
|
if (shouldNotify) {
|
|
showToast(message, "error");
|
|
}
|
|
modalController?.completeError(message);
|
|
await ackComponentActionJob(jobId);
|
|
return;
|
|
}
|
|
handledComponentJobs.add(jobId);
|
|
capSetSize(handledComponentJobs);
|
|
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 || []) {
|
|
const jobId = state.job_id;
|
|
if (!jobId) {
|
|
continue;
|
|
}
|
|
if (handledComponentJobs.has(jobId) || followedComponentJobs.has(jobId)) {
|
|
continue;
|
|
}
|
|
if (state.running) {
|
|
void followComponentActionState(state);
|
|
} else {
|
|
handledComponentJobs.add(jobId);
|
|
capSetSize(handledComponentJobs);
|
|
void ackComponentActionJob(jobId);
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
let modalController = getActiveProgressModalController();
|
|
if (!modalController) {
|
|
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;
|
|
if (followedComponentJobs.has(jobId) || handledComponentJobs.has(jobId)) {
|
|
return;
|
|
}
|
|
followedComponentJobs.add(jobId);
|
|
ownsJobFollow = true;
|
|
setActiveProgressModalJobId(jobId);
|
|
markUiActionOwned("component", jobId);
|
|
modalController.startLogTracking(jobId);
|
|
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: space-between;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 6px;
|
|
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
|
padding-top: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.tachyon-update-modal__action-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.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",
|
|
`
|
|
<style id="${TACHYON_GLOBAL_STYLES_ID}">
|
|
${GlobalStyles}
|
|
</style>
|
|
`
|
|
);
|
|
}
|
|
|
|
// 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
|
|
});
|