// This file is autogenerated, please don't change manually "use strict"; "require baseclass"; "require fs"; "require uci"; "require ui"; // 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 validateIPV4(ip) { if (isIPv4(ip)) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _("Invalid IP address") }; } function validateIPv6(ip) { if (isIPv6(ip)) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _("Invalid IP address") }; } 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 parseHostPort(value) { if (!value) { return null; } if (value.startsWith("[")) { const end = value.indexOf("]"); if (end <= 1 || value.slice(end + 1, end + 2) !== ":") { return null; } return { host: value.slice(1, end), port: value.slice(end + 2) }; } const firstColon = value.indexOf(":"); const lastColon = value.lastIndexOf(":"); if (firstColon < 0 || firstColon !== lastColon) { return null; } return { host: value.slice(0, firstColon), port: value.slice(firstColon + 1) }; } // 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 (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; const portNum = parsed.port ? Number(parsed.port) : 0; if ((isValidHost(host) || host === "localhost") && (!parsed.port || Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535)) { 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") }; } const portNum = parseInt(port, 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { 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("/", ""); const portNum = Number(cleanedPort); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) 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") { return { valid: false, message: "Invalid VMess URL: missing server" }; } const portNum = Number(port); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { 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" }; const portNum = Number(port); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) 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") }; } const portNum = Number(port); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { return { valid: false, message: _("Invalid SOCKS URL: invalid port number") }; } if (!isValidHost(host)) { return { valid: false, message: _("Invalid SOCKS URL: invalid host format") }; } } 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 isValidPortNumber = (value) => { if (!/^\d+$/.test(value)) { return false; } const portNum = Number(value); return Number.isInteger(portNum) && portNum >= 1 && portNum <= 65535; }; const isValidPortEntry = (entry) => { if (!entry) { return false; } if (!entry.includes("-")) { return isValidPortNumber(entry); } const rangeParts = entry.split("-"); if (rangeParts.length !== 2) { return false; } const [start, end] = rangeParts; return isValidPortNumber(start) && isValidPortNumber(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/validateHttpProxyUrl.ts function validateHttpProxyUrl(url) { try { if (!/^https?:\/\//.test(url)) { return { valid: false, message: _( "Invalid HTTP proxy URL: must start with http:// or https://" ) }; } if (!url || /\s/.test(url)) { return { valid: false, message: _("Invalid HTTP proxy URL: must not contain spaces") }; } const body = url.replace(/^https?:\/\//, ""); if (/[/?#]/.test(body)) { return { valid: false, message: _( "Invalid HTTP proxy URL: path, query, and fragment are not supported" ) }; } const atIndex = body.lastIndexOf("@"); const credentials = atIndex >= 0 ? body.slice(0, atIndex) : ""; const hostPortPart = atIndex >= 0 ? body.slice(atIndex + 1) : body; if (credentials) { const [username] = credentials.split(":"); if (!username) { return { valid: false, message: _("Invalid HTTP proxy URL: missing username") }; } } if (!hostPortPart) { return { valid: false, message: _("Invalid HTTP proxy URL: missing host and port") }; } const parsedHostPort = parseHostPort(hostPortPart); if (!parsedHostPort) { return { valid: false, message: _("Invalid HTTP proxy URL: invalid host and port") }; } const { host, port } = parsedHostPort; if (!host) { return { valid: false, message: _("Invalid HTTP proxy URL: missing hostname or IP") }; } if (!port) { return { valid: false, message: _("Invalid HTTP proxy URL: missing port") }; } const portNum = Number(port); if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { return { valid: false, message: _("Invalid HTTP proxy URL: invalid port number") }; } if (!isValidHost(host)) { return { valid: false, message: _("Invalid HTTP proxy URL: invalid host format") }; } } catch (_e) { return { valid: false, message: _("Invalid HTTP proxy URL: parsing failed") }; } return { valid: true, message: _("Valid") }; } // 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 (/^https?:\/\//.test(trimmedUrl)) { return validateHttpProxyUrl(trimmedUrl); } if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { return validateHysteria2Url(trimmedUrl); } return { valid: false, message: _( "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://" ) }; } // 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/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/forkop/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); switch (level) { case "error": console.error(message); break; case "warn": console.warn(message); break; case "info": console.info(message); break; default: 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.info("[SHELL]", `[${operationName}] took ${elapsed.toFixed(2)} ms`); } } // src/constants.ts var FORKOP_UCI_PACKAGE = "forkop"; var FORKOP_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__"; var FORKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT = "forkop: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 REGIONAL_OPTIONS = [ "russia_inside", "russia_outside", "ukraine_inside" ]; var ALLOWED_WITH_RUSSIA_INSIDE = [ "russia_inside", "meta", "twitter", "discord", "telegram", "cloudflare", "google_ai", "google_play", "hetzner", "ovh", "hodca", "roblox", "digitalocean", "cloudfront" ]; 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_SERVER_OPTIONS = { "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)", "dns.adguard-dns.com": "dns.adguard-dns.com (AdGuard Default)", "unfiltered.adguard-dns.com": "unfiltered.adguard-dns.com (AdGuard Unfiltered)", "family.adguard-dns.com": "family.adguard-dns.com (AdGuard Family)" }; var BOOTSTRAP_DNS_SERVER_OPTIONS = { "77.88.8.8": "77.88.8.8 (Yandex DNS)", "77.88.8.1": "77.88.8.1 (Yandex DNS)", "1.1.1.1": "1.1.1.1 (Cloudflare DNS)", "1.0.0.1": "1.0.0.1 (Cloudflare DNS)", "8.8.8.8": "8.8.8.8 (Google DNS)", "8.8.4.4": "8.8.4.4 (Google DNS)", "9.9.9.9": "9.9.9.9 (Quad9 DNS)", "9.9.9.11": "9.9.9.11 (Quad9 DNS)" }; var COMMAND_TIMEOUT = 1e4; // src/helpers/executeShellCommand.ts async function executeShellCommand({ command, args, timeout = COMMAND_TIMEOUT }) { try { return await withTimeout( fs.exec(command, args), timeout, [command, ...args].join(" ") ); } catch (err) { const error = err; const code = typeof error?.code === "number" ? error.code : 1; return { stdout: "", stderr: error?.message, code }; } } // src/helpers/onMount.ts function getTarget(target) { if (typeof target === "string") { return document.getElementById(target); } return target; } async function onMount(target) { return new Promise((resolve) => { let observer = null; const resolveIfMountedAndVisible = () => { const mountedTarget = getTarget(target); if (mountedTarget && mountedTarget.isConnected && mountedTarget.offsetParent !== null) { observer?.disconnect(); resolve(mountedTarget); return true; } return false; }; if (resolveIfMountedAndVisible()) { return; } observer = new MutationObserver(() => { resolveIfMountedAndVisible(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden"] }); }); } // src/helpers/getClashApiUrl.ts function getWindowLocation() { return typeof window !== "undefined" ? window.location : void 0; } function canUseDirectClashApi() { const location = getWindowLocation(); return typeof location?.hostname === "string" && location.hostname !== "" && location.protocol !== "https:"; } function getClashWsUrl() { const { hostname } = window.location; return `ws://${hostname}:9090`; } function getClashHttpUrl() { const { hostname } = window.location; return `http://${hostname}:9090`; } function getClashUIUrl() { const { hostname } = window.location; return `http://${hostname}:9090/ui`; } // src/helpers/preserveScrollForPage.ts function preserveScrollForPage(renderFn) { const scrollY = window.scrollY; renderFn(); requestAnimationFrame(() => { window.scrollTo({ top: scrollY }); }); } // src/helpers/svgEl.ts function svgEl(tag, attrs = {}, children = []) { const NS = "http://www.w3.org/2000/svg"; const el = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) { if (v != null) el.setAttribute(k, String(v)); } (Array.isArray(children) ? children : [children]).filter(Boolean).forEach((ch) => el.appendChild(ch)); return el; } // src/helpers/insertIf.ts function insertIf(condition, elements) { return condition ? elements : []; } // src/helpers/isCopyableProxyLink.ts var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i; var HTTP_PROXY_URI_RE = /^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i; function isCopyableProxyLink(link) { const value = (link || "").trim(); if (COPYABLE_PROXY_URI_RE.test(value)) { return true; } const httpProxy = value.match(HTTP_PROXY_URI_RE); if (!httpProxy) { return false; } const port = Number(httpProxy[1]); return Number.isInteger(port) && port >= 1 && port <= 65535; } // 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/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/forkop/tabs/dashboard/partials/renderSections.ts function renderFailedState() { return E( "div", { class: "fkp_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: "fkp_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 fkp_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: "fkp_dashboard-page__subscription-meta" }, [ E("div", { class: "fkp_dashboard-page__subscription-meta__main" }, [ E( "div", { class: "fkp_dashboard-page__subscription-meta__heading" }, _("Subscription info:") ), title ? E( "div", { class: "fkp_dashboard-page__subscription-meta__title" }, title ) : "", rows.length ? E( "div", { class: "fkp_dashboard-page__subscription-meta__facts" }, rows.map( (row) => E( "div", { class: "fkp_dashboard-page__subscription-meta__fact" }, [ E( "span", { class: "fkp_dashboard-page__subscription-meta__fact-key" }, row.label ), E( "span", { class: "fkp_dashboard-page__subscription-meta__fact-value" }, row.value ) ] ) ) ) : "", actions.length ? E( "div", { class: "fkp_dashboard-page__subscription-meta__actions" }, actions ) : "" ]), metadata.announce ? E( "blockquote", { class: "fkp_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 fkp_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 }) { 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 (!outbound.latency) { return "fkp_dashboard-page__outbound-grid__item__latency--empty"; } if (outbound.latency < 800) { return "fkp_dashboard-page__outbound-grid__item__latency--green"; } if (outbound.latency < 1500) { return "fkp_dashboard-page__outbound-grid__item__latency--yellow"; } return "fkp_dashboard-page__outbound-grid__item__latency--red"; } 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 = [ "fkp_dashboard-page__outbound-grid__item", outbound.selected ? "fkp_dashboard-page__outbound-grid__item--active" : "", canChooseOutbound ? "fkp_dashboard-page__outbound-grid__item--selectable" : "", section.withTagSelect && !canChooseOutbound ? "fkp_dashboard-page__outbound-grid__item--disabled" : "", outboundSwitching ? "fkp_dashboard-page__outbound-grid__item--switching" : "" ].filter(Boolean).join(" "); 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: "fkp_dashboard-page__outbound-grid__item__snake" }, [ svgEl("rect", { width: "100%", height: "100%", fill: "none", rx: 4, ry: 4, pathLength: 100 }) ] ) ] : [], E("div", { class: "fkp_dashboard-page__outbound-grid__item__header" }, [ E("b", {}, outbound.displayName), ...canCopyLink ? [ E( "button", { type: "button", class: "btn fkp_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 fkp_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 fkp_dashboard-page__outbound-grid__item__copy-button", title: _("Priority details"), "aria-label": _("Priority details"), click: (event) => { event.stopPropagation(); onShowPriorityInfo(section, outbound); } }, renderInfoIcon24() ) ] : [] ]), E("div", { class: "fkp_dashboard-page__outbound-grid__item__footer" }, [ E( "div", { class: "fkp_dashboard-page__outbound-grid__item__type" }, [outbound.type].filter(Boolean) ), E( "div", { class: getLatencyClass() }, outbound.latency ? `${outbound.latency}ms` : "N/A" ) ]) ] ); } const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean); const subscriptionUpdateAction = renderSubscriptionUpdateAction( section, subscriptionUpdating, onUpdateSubscription ); return E("div", { class: "fkp_dashboard-page__outbound-section" }, [ // Title with test latency E("div", { class: "fkp_dashboard-page__outbound-section__title-section" }, [ E( "div", { class: "fkp_dashboard-page__outbound-section__title-section__title" }, section.displayName ), E( "div", { class: "fkp_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" }, getLatencyTestLabel(latencyProgress) ) ] : E( "span", { class: "dashboard-sections-grid-item-test-latency__label" }, _("Test latency") ) ) ] ) ]), E("div", { class: "fkp_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/forkop/tabs/dashboard/partials/renderWidget.ts function renderFailedState2() { return E( "div", { id: "", style: "height: 78px", class: "fkp_dashboard-page__widgets-section__item centered" }, _("Currently unavailable") ); } function renderLoadingState2() { return E( "div", { id: "", style: "height: 78px", class: "fkp_dashboard-page__widgets-section__item skeleton" }, "" ); } function renderDefaultState2({ title, items }) { return E("div", { class: "fkp_dashboard-page__widgets-section__item" }, [ E( "b", { class: "fkp_dashboard-page__widgets-section__item__title" }, title ), ...items.map( (item) => E( "div", { class: `fkp_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` }, [ E( "span", { class: "fkp_dashboard-page__widgets-section__item__row__key" }, `${item.key}: ` ), E( "span", { class: "fkp_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/forkop/tabs/dashboard/render.ts function render() { return E( "div", { id: "dashboard-status", class: "fkp_dashboard-page" }, [ E( "div", { class: "fkp_dashboard-page__service-stopped", role: "status" }, _( "Forkop service is stopped. Start the service to display the dashboard." ) ), E("div", { class: "fkp_dashboard-page__content" }, [ // Widgets section E("div", { class: "fkp_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: [] }) ) ]), // All outbounds E( "div", { id: "dashboard-sections-grid" }, renderSections({ loading: true, failed: false, section: { code: "", sectionName: "", displayName: "", outbounds: [], withTagSelect: false }, onTestLatency: () => { }, onChooseOutbound: () => { }, onCopyOutbound: () => { }, onShowUrlTestInfo: () => { }, onShowPriorityInfo: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, latencyProgress: void 0, subscriptionUpdating: false, selectorSwitchingTag: void 0 }) ) ]) ] ); } // src/helpers/showToast.ts function showToast(message, type, duration = 3e3) { let container = document.querySelector(".toast-container"); if (!container) { container = document.createElement("div"); container.className = "toast-container"; document.body.appendChild(container); } const toast = document.createElement("div"); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => toast.classList.add("visible"), 100); setTimeout(() => { toast.classList.remove("visible"); setTimeout(() => toast.remove(), 300); }, duration); } // src/helpers/copyToClipboard.ts function copyToClipboard(text) { const textarea = document.createElement("textarea"); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); showToast(_("Copied"), "success"); } catch (_err) { showToast(_("Failed to copy!"), "error"); console.error("copyToClipboard - e", _err); } document.body.removeChild(textarea); } // src/forkop/methods/custom/getConfigSections.ts async function getConfigSections() { return uci.load(FORKOP_UCI_PACKAGE).then(() => uci.sections(FORKOP_UCI_PACKAGE)); } // src/forkop/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/forkop/methods/shell/callBaseMethod.ts async function callBaseMethod(method, args = [], command = "/usr/bin/forkop", 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/forkop/types.ts var Forkop; ((Forkop2) => { 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_LINK"] = "get_outbound_link"; AvailableMethods2["GET_OUTBOUND_LINK_STATES"] = "get_outbound_link_states"; 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["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_UPDATE_CHECK_CACHE"] = "component_update_check_cache"; AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async"; AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status"; })(AvailableMethods = Forkop2.AvailableMethods || (Forkop2.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 = Forkop2.AvailableClashAPIMethods || (Forkop2.AvailableClashAPIMethods = {})); })(Forkop || (Forkop = {})); // src/forkop/helpers/isTransientRpcError.ts var TRANSIENT_RPC_ERROR_PATTERNS = [ "no related rpc reply", "request aborted", "operation was aborted" ]; function isTransientRpcError(message) { if (!message) { return false; } const normalized = message.toLowerCase(); return TRANSIENT_RPC_ERROR_PATTERNS.some( (pattern) => normalized.includes(pattern) ); } // src/forkop/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 = 15e3; var COMPONENT_ACTION_POLL_INTERVAL_MS = 1500; var COMPONENT_ACTION_STATUS_REFRESH_INTERVAL_MS = 15e3; var COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS = 3e4; var COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS = 3e4; var COMPONENT_ACTION_STATE_DIR = "/var/run/forkop/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 readForkopVersion() { const response = await executeShellCommand({ command: "/usr/bin/forkop", 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( Forkop.AvailableMethods.GET_UI_STATE, [], "/usr/bin/forkop", { timeout: GET_UI_STATE_RPC_TIMEOUT_MS } ); return response.success && response.data.actions.component.some( (state) => state.job_id === jobId && state.component === component && state.action === action && state.running === true ); } function componentActionFailure(response, parsedResponse) { return { success: false, error: parsedResponse?.message || response.stderr || _("Failed to execute") }; } function uiActionFailure(response, parsedResponse, fallback = _("Failed to execute")) { return { success: false, error: parsedResponse?.message || response.stderr || fallback }; } function createTransientRpcGraceTracker(graceMs) { let failureStartedAt = 0; return { reset() { failureStartedAt = 0; }, shouldContinue(error) { if (!isTransientRpcError(error)) { failureStartedAt = 0; return false; } if (!failureStartedAt) { failureStartedAt = Date.now(); } return Date.now() - failureStartedAt < graceMs; } }; } var ForkopShellMethods = { checkDNSAvailable: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_DNS_AVAILABLE ), checkFakeIP: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_FAKEIP ), checkNftRules: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_NFT_RULES ), checkZapretRuntime: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_ZAPRET_RUNTIME ), checkZapret2Runtime: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_ZAPRET2_RUNTIME ), checkByedpiRuntime: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_BYEDPI_RUNTIME ), checkInboundsConfig: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_INBOUNDS_CONFIG ), getStatus: async () => callBaseMethod(Forkop.AvailableMethods.GET_STATUS), getOutboundLink: async (section, tag) => callBaseMethod( Forkop.AvailableMethods.GET_OUTBOUND_LINK, [section, tag] ), getOutboundLinkStates: async (section) => callBaseMethod( Forkop.AvailableMethods.GET_OUTBOUND_LINK_STATES, [section] ), getOutboundMetadata: async (section) => callBaseMethod( Forkop.AvailableMethods.GET_OUTBOUND_METADATA, [section] ), getSubscriptionMetadata: async (section) => callBaseMethod( Forkop.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section] ), checkSingBox: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_SING_BOX ), checkInbounds: async () => callBaseMethod( Forkop.AvailableMethods.CHECK_INBOUNDS ), getSingBoxStatus: async () => callBaseMethod( Forkop.AvailableMethods.GET_SING_BOX_STATUS ), getZapretStatus: async () => callBaseMethod( Forkop.AvailableMethods.GET_ZAPRET_STATUS ), getZapret2Status: async () => callBaseMethod( Forkop.AvailableMethods.GET_ZAPRET2_STATUS ), getByedpiStatus: async () => callBaseMethod( Forkop.AvailableMethods.GET_BYEDPI_STATUS ), getClashApiProxies: async () => callBaseMethod(Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.GET_PROXIES ]), getClashApiConnections: async () => callBaseMethod(Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.GET_CONNECTIONS ]), getClashApiProxyLatency: async (tag, timeout = "5000") => callBaseMethod( Forkop.AvailableMethods.CLASH_API, [Forkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, timeout] ), getClashApiProxyLatencies: async (tags) => callBaseMethod( Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.GET_PROXY_LATENCIES, JSON.stringify(tags), "5000" ] ), getClashApiGroupLatency: async (tag) => callBaseMethod( Forkop.AvailableMethods.CLASH_API, [Forkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"] ), setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.SET_GROUP_PROXY, group, proxy ]), closeClashApiConnection: async (connectionId) => callBaseMethod(Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.CLOSE_CONNECTION, connectionId ]), closeAllClashApiConnections: async () => callBaseMethod(Forkop.AvailableMethods.CLASH_API, [ Forkop.AvailableClashAPIMethods.CLOSE_ALL_CONNECTIONS ]), enable: async () => callBaseMethod( Forkop.AvailableMethods.ENABLE, [], "/etc/init.d/forkop" ), disable: async () => callBaseMethod( Forkop.AvailableMethods.DISABLE, [], "/etc/init.d/forkop" ), globalCheck: async (masked = true) => callBaseMethod(Forkop.AvailableMethods.GLOBAL_CHECK, [ masked ? "masked" : "raw" ]), showSingBoxConfig: async (masked = true) => callBaseMethod(Forkop.AvailableMethods.SHOW_SING_BOX_CONFIG, [ masked ? "masked" : "raw" ]), checkLogs: async () => callBaseMethod(Forkop.AvailableMethods.CHECK_LOGS), checkSingBoxLogs: async () => callBaseMethod(Forkop.AvailableMethods.CHECK_SING_BOX_LOGS), getSystemInfo: async () => callBaseMethod( Forkop.AvailableMethods.GET_SYSTEM_INFO ), getServerCapabilities: async () => callBaseMethod( Forkop.AvailableMethods.GET_SERVER_CAPABILITIES ), getUiCapabilities: async () => callBaseMethod( Forkop.AvailableMethods.GET_UI_CAPABILITIES ), getUiState: async () => callBaseMethod( Forkop.AvailableMethods.GET_UI_STATE, [], "/usr/bin/forkop", { timeout: GET_UI_STATE_RPC_TIMEOUT_MS } ), serviceActionStart: async (action) => { const response = await executeShellCommand({ command: "/usr/bin/forkop", args: [Forkop.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/forkop", args: [Forkop.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 ForkopShellMethods.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/forkop", args: [ Forkop.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/forkop", args: [Forkop.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 ForkopShellMethods.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/forkop", args: [Forkop.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/forkop", args: [Forkop.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/forkop", args: [Forkop.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 }; }, componentUpdateCheckCache: async () => callBaseMethod( Forkop.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE ), waitComponentActionJob: async (jobId, component, action, expectedLatestVersion) => { let selfUpdateVersionMatchedAt = 0; let lastStatusRefreshAt = 0; const transientRpc = createTransientRpcGraceTracker( COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS ); while (true) { await sleep(COMPONENT_ACTION_POLL_INTERVAL_MS); const stateResponse = await readComponentActionState(jobId); if (stateResponse) { if (!stateResponse.running) { transientRpc.reset(); return { success: true, data: stateResponse }; } if (Date.now() - lastStatusRefreshAt < COMPONENT_ACTION_STATUS_REFRESH_INTERVAL_MS) { continue; } } lastStatusRefreshAt = Date.now(); const statusResponse = await executeShellCommand({ command: "/usr/bin/forkop", args: [Forkop.AvailableMethods.COMPONENT_ACTION_STATUS, jobId], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseComponentActionResult(statusResponse); if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) { if (stateResponse?.running) { transientRpc.reset(); continue; } if (await isComponentActionStillRunning(jobId, component, action)) { transientRpc.reset(); continue; } const failure = componentActionFailure(statusResponse, parsedResponse); if (transientRpc.shouldContinue(failure.error)) { continue; } if (component === "forkop" && action === "install") { const installedVersion = expectedLatestVersion ? await readForkopVersion() : ""; if (expectedLatestVersion && installedVersion === expectedLatestVersion) { if (!selfUpdateVersionMatchedAt) { selfUpdateVersionMatchedAt = Date.now(); } if (Date.now() - selfUpdateVersionMatchedAt >= COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS) { return { success: true, data: { success: true, component, action, message: translate("Forkop has been installed"), current_version: installedVersion, latest_version: expectedLatestVersion, changed: true, status: "latest" } }; } } continue; } return failure; } transientRpc.reset(); if (parsedResponse.running) { continue; } return { success: true, data: parsedResponse }; } }, subscriptionUpdateStart: async (section, sourceIndex) => { const startArgs = [ Forkop.AvailableMethods.SUBSCRIPTION_UPDATE_ASYNC, ...section ? [section] : [], ...section && sourceIndex !== void 0 ? [String(sourceIndex)] : [] ]; const response = await executeShellCommand({ command: "/usr/bin/forkop", 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/forkop", args: [Forkop.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 ForkopShellMethods.subscriptionUpdateStatus(jobId); if (!response.success) { if (transientRpc.shouldContinue(response.error)) { continue; } return response; } transientRpc.reset(); if (response.data.running) { continue; } return response; } } }; // src/forkop/methods/custom/getDashboardSections.ts var DASHBOARD_SECTION_CACHE_DIR = "/var/run/forkop/section-cache"; function getDisplayName(section) { return section.label || section[".name"]; } function getSectionAction(section) { return section.action || ""; } 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); try { const response = await fetch(`${getClashHttpUrl()}/proxies`, { headers: secret ? { Authorization: `Bearer ${secret}` } : void 0 }); if (response.ok) { return { success: true, data: await response.json() }; } } catch (_error) { } } return ForkopShellMethods.getClashApiProxies(); } function getListValues(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map((item) => `${item}`.trim()).filter(Boolean); } return `${value}`.split(/\s+/).map((item) => item.trim()).filter(Boolean); } function childSections(configSections, type) { return configSections.filter((section) => section[".type"] === type); } function ownedChildSections(parent, children) { return children.filter( (section) => section.section === parent[".name"] ); } function childSectionsByOwner(children, ownerKey, ownerValue) { return children.filter((section) => section[ownerKey] === ownerValue); } function compactSettingsMap(settings) { return Object.keys(settings).length ? JSON.stringify(settings) : void 0; } function hydrateConfigSections(configSections) { const subscriptionUrls = childSections(configSections, "subscription_url"); const interfaces = childSections(configSections, "section_interface"); const urltests = childSections(configSections, "urltest"); const priorityGroups = childSections(configSections, "priority_group"); const priorityLevels = childSections(configSections, "priority_level"); return configSections.map((section) => { if (section[".type"] !== "section") { return section; } const next = { ...section }; const subscriptionUrlItems = ownedChildSections(next, subscriptionUrls); const interfaceItems = ownedChildSections(next, interfaces); const urltestItems = ownedChildSections(next, urltests); const priorityGroupItems = ownedChildSections(next, priorityGroups); if (subscriptionUrlItems.length) { const settings = {}; next.subscription_urls = subscriptionUrlItems.map((item) => item.url || "").filter(Boolean); subscriptionUrlItems.forEach((item) => { if (!item.url) { return; } settings[item.url] = { subscription_update_enabled: item.subscription_update_enabled, subscription_update_interval: item.subscription_update_interval, download_via_proxy_enabled: item.download_via_proxy_enabled, download_via_proxy_section: item.download_via_proxy_section, auto_user_agent: item.auto_user_agent, user_agent: item.user_agent, auto_hwid: item.auto_hwid, hwid: item.hwid, show_dashboard_metadata: item.show_dashboard_metadata, prefix_nodes: item.prefix_nodes, node_prefix: item.node_prefix, include_urltest_groups: item.include_urltest_groups, hide_urltest_group_outbounds: item.hide_urltest_group_outbounds, hide_detour_outbounds: item.hide_detour_outbounds }; }); next.subscription_url_settings = compactSettingsMap(settings); } if (interfaceItems.length) { next.interfaces = interfaceItems.map((item) => item.name || "").filter(Boolean); } if (urltestItems.length) { const settings = {}; next.urltests = urltestItems.map((item) => item[".name"]); urltestItems.forEach((item) => { settings[item[".name"]] = { display_name: item.name || item.display_name, urltest_check_interval: item.check_interval, urltest_tolerance: item.tolerance, urltest_testing_url: item.testing_url, idle_timeout: item.idle_timeout, interrupt_exist_connections: item.interrupt_exist_connections, pin_dashboard: item.pin_dashboard, hide_added_outbounds: item.hide_added_outbounds, 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, hide_added_outbounds: item.hide_added_outbounds, 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 ["connection", "proxy", "outbound", "vpn"].includes(action); } function hasSubscriptionSources(section) { return getSubscriptionSourceCount(section) > 0; } function getSubscriptionSourceCount(section) { return getListValues(section.subscription_urls).length; } function shouldSortByLatency(section) { return section.sort_by_latency === "1"; } function hasConfiguredUrlTestList(section) { return getListValues(section.urltests).length > 0; } function hasConfiguredPriorityList(section) { return getListValues(section.priority_groups).length > 0; } function getUrlTestIds(section) { const values = getListValues(section.urltests); return values.length ? values : section.urltest_enabled === "1" ? ["urltest"] : []; } function isUrlTestEnabled(section) { return getUrlTestIds(section).length > 0; } function shouldUseProxyGroup(section) { return getManualProxyLinks(section).length > 0 || hasSubscriptionSources(section) || getConnectionInterfaces(section).length > 0 || getJsonOutbounds(section).length > 0 || isUrlTestEnabled(section) || hasConfiguredPriorityList(section); } function getSectionProxyConfigType(section) { if (hasSubscriptionSources(section)) { return "subscription"; } if (isUrlTestEnabled(section) && shouldUseProxyGroup(section)) { return "urltest"; } if (getManualProxyLinks(section).length > 0) { return "selector"; } if (getJsonOutbounds(section).length > 0) { return "outbound"; } if (getConnectionInterfaces(section).length > 0) { return "interface"; } if (hasConfiguredPriorityList(section)) { return "selector"; } return void 0; } function getJsonOutboundDisplayName(section) { try { const parsedOutbound = JSON.parse(section.outbound_json || "{}"); return parsedOutbound?.tag ? decodeURIComponent(parsedOutbound.tag) : ""; } catch (_error) { return ""; } } function buildManualLinkByCode(section) { const sectionName = section[".name"]; return new Map( getManualProxyLinks(section).map((link, index) => [ getOutboundTagBySection(`${sectionName}-${index + 1}`), link ]) ); } function getProxyEntryByCode(proxies) { return new Map(proxies.map((proxy) => [proxy.code, proxy])); } function uniqueCodes(codes) { return Array.from(new Set(codes.filter(Boolean))); } function isSelectorOutbound(outbound) { return outbound.type?.toLowerCase() === "selector"; } function isUrlTestProxyEntry(entry) { return entry?.value?.type?.toLowerCase() === "urltest"; } function getLatencySortValue(outbound) { const latency = Number(outbound.latency); return Number.isFinite(latency) && latency > 0 ? latency : Number.POSITIVE_INFINITY; } function sortOutboundsForDashboard(outbounds, options = {}) { const pinnedCodes = [ ...options.pinnedCode ? [options.pinnedCode] : [], ...options.pinnedCodes || [] ].filter(Boolean); const pinnedRank = new Map( pinnedCodes.map((code, index) => [code, index]) ); const sortByLatency = options.sortByLatency === true; return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => { const leftPinned = pinnedRank.has(left.outbound.code); const rightPinned = pinnedRank.has(right.outbound.code); if (leftPinned !== rightPinned) { return leftPinned ? -1 : 1; } if (leftPinned && rightPinned) { const rankDiff = (pinnedRank.get(left.outbound.code) ?? 0) - (pinnedRank.get(right.outbound.code) ?? 0); if (rankDiff !== 0) { return rankDiff; } } if (sortByLatency) { const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound); if (latencyDiff !== 0) { return latencyDiff; } } return left.index - right.index; }).map((item) => item.outbound); } function sortUrlTestMembers(outbounds) { return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => { if (left.outbound.selected !== right.outbound.selected) { return left.outbound.selected ? -1 : 1; } const latencyDiff = getLatencySortValue(left.outbound) - getLatencySortValue(right.outbound); if (latencyDiff !== 0) { return latencyDiff; } return left.index - right.index; }).map((item) => item.outbound); } function isSafeSectionName(sectionName) { return /^[A-Za-z0-9_-]+$/.test(sectionName); } function objectMap(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return {}; } return Object.fromEntries( Object.entries(value).filter(([, item]) => typeof item === "string").map(([key, item]) => [key, item]) ); } function itemSettingsMap(value) { if (!value) { return {}; } try { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return Object.fromEntries( Object.entries(parsed).filter( ([, item]) => item && typeof item === "object" && !Array.isArray(item) ) ); } catch (_error) { return {}; } } function itemSettingString(settings, key, fallback = "") { const value = settings?.[key]; return typeof value === "string" ? value : fallback; } function itemSettingBoolean(settings, key, fallback) { const value = settings?.[key]; if (value === void 0 || value === null || value === "") { return fallback; } return value === "1" || value === "true"; } function isUrlTestFilteringEnabled(settings) { return ["exclude", "include", "mixed"].includes( itemSettingString(settings, "urltest_filter_mode", "disabled") ); } function getUrlTestTag(sectionName, id) { return getOutboundTagBySection( id === "urltest" ? `${sectionName}-urltest` : `${sectionName}-urltest-${id}` ); } function getPriorityTag(sectionName, id) { return getOutboundTagBySection(`${sectionName}-priority-${id}`); } function getUrlTestDisplayName(section, id, settings) { return itemSettingString( settings, "display_name", id === "urltest" && !hasConfiguredUrlTestList(section) ? _("Fastest") : id ); } function getUrlTestConfigs(section) { const settingsMap = itemSettingsMap(section.urltest_settings); const sectionName = section[".name"]; return getUrlTestIds(section).map((id) => { const settings = settingsMap[id] || {}; const filteringEnabled = isUrlTestFilteringEnabled(settings); return { id, code: getUrlTestTag(sectionName, id), displayName: getUrlTestDisplayName(section, id, settings), settings, pinDashboard: itemSettingBoolean(settings, "pin_dashboard", true), hideAddedOutbounds: itemSettingBoolean( settings, "hide_added_outbounds", false ), 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), hideAddedOutbounds: itemSettingBoolean( settings, "hide_added_outbounds", false ), 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, subscriptionCopyableCodes, showDetectedCountries }) { const childCodes = uniqueCodes( groupCache?.outbounds?.length ? groupCache.outbounds : entry?.value.all || [] ); const selectedCode = entry?.value.now || ""; const outbounds = sortUrlTestMembers( childCodes.flatMap((childCode) => { const childEntry = proxyByCode.get(childCode); const link = manualLinkByCode.get(childCode) || cachedProxyLinks.get(childCode) || ""; const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(childCode); return [ { code: childCode, displayName: getOutboundDisplayName( childCode, childEntry, link, outboundMetadata, subscriptionCopyableCodes.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, 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, subscriptionCopyableCodes, showDetectedCountries }) { const selectedCode = entry?.value.now || ""; 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 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) || subscriptionCopyableCodes.has(childCode); return { code: childCode, displayName: getOutboundDisplayName( childCode, childEntry, link, outboundMetadata, subscriptionCopyableCodes.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, 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 = {}, subscriptionCopyableCodes = /* @__PURE__ */ new Set(), 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 urlTestCodeSet = new Set(urlTestCodes); const priorityCodeSet = new Set(priorityCodes); const hideAddedCodeSet = /* @__PURE__ */ new Set(); urlTestEntries.forEach(({ config, entry }) => { if (!config.hideAddedOutbounds) { return; } const childCodes = urltestGroups[config.code]?.outbounds?.length ? urltestGroups[config.code].outbounds || [] : entry?.value.all || []; childCodes.forEach((code) => hideAddedCodeSet.add(code)); }); priorityEntries.forEach(({ config, entry }) => { if (!config.hideAddedOutbounds) { return; } const childCodes = priorityGroups[config.code]?.outbounds?.length ? priorityGroups[config.code].outbounds || [] : entry?.value.all || []; childCodes.forEach((code) => hideAddedCodeSet.add(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 ]).filter((code) => { if (!hideAddedCodeSet.has(code)) { return true; } return urlTestCodeSet.has(code) || priorityCodeSet.has(code) || isUrlTestProxyEntry(proxyByCode.get(code)); }); 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) || subscriptionCopyableCodes.has(code); const displayName = priorityConfig?.displayName || urlTestConfig?.displayName || getOutboundDisplayName( code, item, link, outboundMetadata, subscriptionCopyableCodes.has(code) ); const isRuntimeUrlTest = isUrlTestProxyEntry(item); return [ { code, displayName, latency: item?.value.history?.[0]?.delay || 0, type: priorityConfig ? "Priority" : item?.value.type || "URLTest", selected: selector?.value?.now === code, link, canCopyLink, country: showDetectedCountries ? outboundMetadata?.countries?.[code] : void 0, runtimeAvailable: item ? void 0 : false, urlTestInfo: urlTestConfig || isRuntimeUrlTest ? buildUrlTestInfo({ code, displayName, entry: item, groupCache: urltestGroups[code], proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, subscriptionCopyableCodes, showDetectedCountries: urlTestConfig?.showDetectedCountries || showDetectedCountries }) : void 0, priorityInfo: priorityConfig ? buildPriorityInfo({ config: priorityConfig, entry: item, groupCache: priorityGroups[code], proxyByCode, manualLinkByCode, cachedProxyLinks, outboundMetadata, subscriptionCopyableCodes, showDetectedCountries: priorityConfig.showDetectedCountries }) : void 0 } ]; }); 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 getSubscriptionCopyableCodes(dashboardCache) { const legacyLinks = objectMap(dashboardCache?.links); const linkRefs = dashboardCache?.linkRefs; const codes = new Set( Object.entries(legacyLinks).filter(([, link]) => isCopyableProxyLink(link)).map(([code]) => code) ); if (linkRefs && typeof linkRefs === "object" && !Array.isArray(linkRefs)) { Object.keys(linkRefs).forEach((code) => codes.add(code)); } return codes; } 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(getSectionAction(section)) ).map(async (section) => { const displayName = getDisplayName(section); const sectionName = section[".name"]; const sectionAction = getSectionAction(section); 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 subscriptionCopyableCodes = includeSubscriptionCopyState ? getSubscriptionCopyableCodes(dashboardCache) : /* @__PURE__ */ new Set(); 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, subscriptionCopyableCodes, cachedProxyLinks ); return { withTagSelect: true, code: selector?.code || sectionName, sectionName, displayName, action: sectionAction, latencyTestCode, latencyTestCodes, proxyConfigType, subscriptionSourceCount, subscriptionMetadata, outbounds }; } if (sectionAction === "vpn") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find((proxy) => proxy.code === outboundTag); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, action: sectionAction, latencyTestTimeout: "10000", outbounds: [ { code: outbound?.code || sectionName, displayName: section.interface || outbound?.value?.name || "", latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false, runtimeAvailable: Boolean(outbound) } ] }; } if (sectionAction === "outbound") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find((proxy) => proxy.code === outboundTag); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, action: sectionAction, outbounds: [ { code: outbound?.code || sectionName, displayName: getJsonOutboundDisplayName(section) || outbound?.value?.name || "", latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false } ] }; } return { withTagSelect: false, code: sectionName, sectionName, displayName, action: sectionAction, outbounds: [] }; }) ); return { success: true, data }; } // src/forkop/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/forkop/methods/custom/index.ts var CustomForkopMethods = { getConfigSections, getDashboardSections, getClashApiSecret: getClashApiSecret2 }; // src/forkop/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/forkop/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/forkop/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/forkop/methods/fakeip/index.ts var RemoteFakeIPMethods = { getFakeIpCheck, getIpCheck }; // src/forkop/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/forkop/tabs/diagnostic/helpers/getCheckTitle.ts function getCheckTitle(name) { return `${name} ${_("checks")}`; } // src/forkop/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/forkop/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, forkop_version: "loading", forkop_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_tailscale: 1, 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 }, viewLogs: { loading: false }, showSingBoxConfig: { loading: false } }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: getDiagnosticsChecks(_("Not running")), updatesActions: { forkopCheck: { loading: false }, forkopInstall: { loading: false }, singBoxCheck: { loading: false }, singBoxInstall: { loading: false }, singBoxInstallExtended: { loading: false }, singBoxInstallExtendedCompressed: { 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: { forkop: { 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/forkop/services/store.service.ts function jsonStableStringify(obj) { return JSON.stringify(obj, (_2, value) => { if (value && typeof value === "object" && !Array.isArray(value)) { return Object.keys(value).sort().reduce( (acc, key) => { acc[key] = value[key]; return acc; }, {} ); } return value; }); } function jsonEqual(a, b) { try { return jsonStableStringify(a) === jsonStableStringify(b); } catch { return false; } } var StoreService = class { constructor(initial) { this.listeners = /* @__PURE__ */ new Set(); this.value = initial; this.initial = structuredClone(initial); } get() { return this.value; } set(next) { const prev = this.value; const diff = {}; for (const key in next) { if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; } if (Object.keys(diff).length === 0) { return; } const merged = { ...prev, ...next }; this.value = merged; this.listeners.forEach((cb) => cb(this.value, prev, diff)); } reset(keys) { const prev = this.value; const next = structuredClone(this.value); if (keys && keys.length > 0) { keys.forEach((key) => { next[key] = structuredClone(this.initial[key]); }); } else { Object.assign(next, structuredClone(this.initial)); } if (jsonEqual(prev, next)) return; this.value = next; const diff = {}; for (const key in next) { if (!jsonEqual(next[key], prev[key])) diff[key] = next[key]; } this.listeners.forEach((cb) => cb(this.value, prev, diff)); } subscribe(cb) { this.listeners.add(cb); cb(this.value, this.value, {}); return () => this.listeners.delete(cb); } unsubscribe(cb) { this.listeners.delete(cb); } patch(key, value) { this.set({ [key]: value }); } getKey(key) { return this.value[key]; } subscribeKey(key, cb) { let prev = this.value[key]; const wrapper = (val) => { if (!jsonEqual(val[key], prev)) { prev = val[key]; cb(val[key]); } }; this.listeners.add(wrapper); return () => this.listeners.delete(wrapper); } }; var initialStore = { tabService: { current: "", all: [] }, bandwidthWidget: { loading: true, failed: false, data: { up: 0, down: 0 } }, trafficTotalWidget: { loading: true, failed: false, data: { downloadTotal: 0, uploadTotal: 0 } }, systemInfoWidget: { loading: true, failed: false, data: { connections: 0, memory: 0 } }, servicesInfoWidget: { loading: true, failed: false, data: { singbox: 0, forkopRunning: 0, forkopEnabled: 0, forkopStatus: "" } }, sectionsWidget: { loading: true, failed: false, latencyFetchingSections: {}, latencyProgressSections: {}, selectorSwitchingSections: {}, subscriptionUpdatingSections: {}, data: [] }, ...initialDiagnosticStore }; var store = new StoreService(initialStore); // src/forkop/services/forkopLogWatcher.service.ts var ForkopLogWatcher = class _ForkopLogWatcher { 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 (!_ForkopLogWatcher.instance) { _ForkopLogWatcher.instance = new _ForkopLogWatcher(); } return _ForkopLogWatcher.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( "[ForkopLogWatcher]", `initialized (interval: ${this.intervalMs}ms)` ); } normalizeLines(raw) { return raw.split("\n").filter(Boolean).slice(-this.maxTrackedLines); } async checkOnce() { if (!this.fetcher) { logger.warn("[ForkopLogWatcher]", "fetcher not found"); return; } if (this.paused) { logger.debug("[ForkopLogWatcher]", "skipped check \u2014 tab not visible"); return; } if (this.checking) { logger.debug( "[ForkopLogWatcher]", "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("[ForkopLogWatcher]", "failed to read logs:", err); } finally { this.checking = false; } } start() { if (this.running) return; if (!this.fetcher) { logger.warn("[ForkopLogWatcher]", "attempted to start without fetcher"); return; } this.running = true; void this.checkOnce(); this.timer = setInterval(() => this.checkOnce(), this.intervalMs); logger.info( "[ForkopLogWatcher]", `started (interval: ${this.intervalMs}ms)` ); } stop() { if (!this.running) return; this.running = false; if (this.timer) clearInterval(this.timer); logger.info("[ForkopLogWatcher]", "stopped"); } pause() { if (!this.running || this.paused) return; this.paused = true; logger.info("[ForkopLogWatcher]", "paused (tab not visible)"); } resume() { if (!this.running || !this.paused) return; this.paused = false; logger.info("[ForkopLogWatcher]", "resumed (tab active)"); void this.checkOnce(); } reset() { this.lastLines = /* @__PURE__ */ new Set(); this.checking = false; logger.info("[ForkopLogWatcher]", "log history reset"); } }; // src/forkop/services/logNotificationDeduper.service.ts var LOG_NOTIFICATION_STORAGE_KEY = "forkop: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 { } } function isErrorLogLine(line) { const lower = line.toLowerCase(); return lower.includes("[error]") || lower.includes("[fatal]"); } function getForkopLogNotification(line) { if (isErrorLogLine(line)) { return { kind: "error", line }; } const update = line.match( /\[component-update\]\s+(forkop|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 (!getForkopLogNotification(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/forkop/helpers/getComponentActionKey.ts var componentActionKeyMap = { "forkop:check_update": "forkopCheck", "forkop:install": "forkopInstall", "sing_box:check_update": "singBoxCheck", "sing_box:install": "singBoxInstall", "sing_box:install_extended": "singBoxInstallExtended", "sing_box:install_extended_compressed": "singBoxInstallExtendedCompressed", "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/forkop/helpers/singBoxVariant.ts function isExtendedSingBoxVersion(version) { return String(version || "").includes("extended"); } 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_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_tailscale: singBoxExtended || value.sing_box_tailscale ? 1 : 0 }; } // src/forkop/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/forkop/services/uiState.service.ts function isRunningAction(state) { return state.running === true; } function getEmptyUpdatesActions() { return { forkopCheck: { loading: false }, forkopInstall: { loading: false }, singBoxCheck: { loading: false }, singBoxInstall: { loading: false }, singBoxInstallExtended: { loading: false }, singBoxInstallExtendedCompressed: { 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_tailscale = uiState.capabilities.sing_box_tailscale; store.set({ servicesInfoWidget: { loading: false, failed: false, data: { singbox: uiState.service.sing_box.running, forkopRunning: uiState.service.forkop.running, forkopEnabled: uiState.service.forkop.enabled, forkopStatus: uiState.service.forkop.status } }, 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/forkop/services/runtimeUiState.service.ts var RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS = 500; var RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS = 1e3; var RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS = 500; var runtimeUiStateRefreshPromise = null; var lastRuntimeUiStateRefreshAt = 0; var lastRuntimeUiState; var runtimeStateResumeRefreshRegistered = false; var runtimeStatePollTimer = null; var runtimeStatePollingStarted = false; var runtimeStateHasRunningAction = false; var runtimeUiStateListeners = /* @__PURE__ */ new Set(); function isDocumentVisible() { return typeof document === "undefined" || !document.visibilityState || document.visibilityState === "visible"; } function hasRunningAction(uiState) { return Object.values(uiState.actions).some( (actions) => actions.some((action) => action.running) ); } function getNextPollDelay() { return runtimeStateHasRunningAction ? RUNTIME_UI_STATE_ACTIVE_POLL_INTERVAL_MS : RUNTIME_UI_STATE_IDLE_POLL_INTERVAL_MS; } function scheduleRuntimeUiStatePoll(delay = getNextPollDelay()) { if (!runtimeStatePollingStarted || runtimeStatePollTimer || typeof window === "undefined") { return; } runtimeStatePollTimer = window.setTimeout(() => { runtimeStatePollTimer = null; void refreshRuntimeUiState().catch(() => void 0).finally(() => { scheduleRuntimeUiStatePoll(); }); }, delay); } function notifyRuntimeUiStateListeners(uiState) { for (const listener of runtimeUiStateListeners) { try { listener(uiState); } catch (error) { logger.error("[RUNTIME_UI_STATE]", "listener failed", error); } } } async function refreshRuntimeUiState({ force = false } = {}) { if (!isDocumentVisible()) { return void 0; } if (runtimeUiStateRefreshPromise) { return runtimeUiStateRefreshPromise; } const now = Date.now(); if (!force && now - lastRuntimeUiStateRefreshAt < RUNTIME_UI_STATE_REFRESH_MIN_INTERVAL_MS) { return void 0; } lastRuntimeUiStateRefreshAt = now; const promise = ForkopShellMethods.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/forkop/services/core.service.ts var LOG_WATCHER_INTERVAL_MS = 1e4; var LOG_WATCHER_START_DELAY_MS = 5e3; function componentDisplayName(component) { const names = { forkop: "Forkop", 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", "fkp-component-update-notification" ); return; } ui.addNotification( _("Forkop Error"), E("div", {}, notification.line), "error", "fkp-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 = ForkopLogWatcher.getInstance(); const logNotificationDeduper = new LogNotificationDeduper(); watcher.init( async () => { const logs = await ForkopShellMethods.checkLogs(); if (logs.success) { return logs.data; } return ""; }, { intervalMs: LOG_WATCHER_INTERVAL_MS, onNewLog: (line) => { if (logNotificationDeduper.shouldNotify(line)) { const notification = getForkopLogNotification(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/forkop/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(); } 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 ); } } this.sockets.clear(); this.listeners.clear(); this.errorListeners.clear(); this.connected.clear(); logger.info("[SOCKET]", "All connections and state have been reset."); } 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)); return; } this.sockets.set(url, ws); this.connected.set(url, false); this.listeners.set(url, /* @__PURE__ */ new Set()); this.errorListeners.set(url, /* @__PURE__ */ new Set()); ws.addEventListener("open", () => { this.connected.set(url, true); 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"); }); 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.sockets.has(url)) { this.connect(url); } if (!this.listeners.has(url)) { this.listeners.set(url, /* @__PURE__ */ new Set()); } this.listeners.get(url)?.add(listener); } unsubscribe(url, listener, onError) { this.listeners.get(url)?.delete(listener); if (onError) { this.errorListeners.get(url)?.delete(onError); } } // 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 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/forkop/services/uiActionNotification.service.ts var UI_ACTION_NOTIFICATION_STORAGE_KEY = "forkop: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/forkop/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 [forkopResult, singboxResult] = await Promise.allSettled([ ForkopShellMethods.getStatus(), ForkopShellMethods.getSingBoxStatus() ]); if (requestId !== latestServicesInfoRequestId) { return; } const forkop = getSettledMethodResponse("getStatus", forkopResult); const singbox = getSettledMethodResponse("getSingBoxStatus", singboxResult); const previousData = store.get().servicesInfoWidget.data; store.set({ servicesInfoWidget: { loading: false, failed: !forkop.success || !singbox.success, data: { singbox: singbox.success ? singbox.data.running : previousData.singbox, forkopRunning: forkop.success ? forkop.data.running : previousData.forkopRunning, forkopEnabled: forkop.success ? forkop.data.enabled : previousData.forkopEnabled, forkopStatus: forkop.success ? forkop.data.status : previousData.forkopStatus } } }); return void 0; } // src/forkop/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/forkop/helpers/restoredActionLoading.ts function shouldShowLoadingForRestoredAction(state) { return state.running === true; } // src/forkop/helpers/serviceAvailability.ts function getServiceAvailability({ loading: loading2, failed: failed2, running }) { if (loading2) { return "loading"; } if (failed2) { return "unavailable"; } return running ? "running" : "stopped"; } // src/forkop/tabs/dashboard/initController.ts 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 pageUnloading = false; var followedSubscriptionJobs = /* @__PURE__ */ new Set(); var followedLatencyJobs = /* @__PURE__ */ new Set(); var handledSubscriptionJobs = /* @__PURE__ */ new Set(); var handledLatencyJobs = /* @__PURE__ */ new Set(); 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 CustomForkopMethods.getDashboardSections(); if (!dashboardMounted || mountId !== dashboardMountId || getDashboardServiceAvailability() === "stopped") { return false; } if (!success) { throw new Error("failed to fetch dashboard sections"); } const current = store.get().sectionsWidget; store.set({ sectionsWidget: { ...current, loading: false, failed: false, data } }); return true; } catch (error) { logger.error("[DASHBOARD]", "fetchDashboardSections: failed", error); if (!dashboardMounted || mountId !== dashboardMountId || getDashboardServiceAvailability() === "stopped") { return false; } const current = store.get().sectionsWidget; store.set({ sectionsWidget: { ...current, loading: false, failed: current.data.length === 0, data: current.data } }); return false; } } async function fetchDashboardSections(options = {}) { if (sectionsRefreshPromise) { if (options.force) { sectionsRefreshQueued = true; } return sectionsRefreshPromise; } const mountId = dashboardMountId; const promise = (async () => { let success = false; do { sectionsRefreshQueued = false; success = await fetchDashboardSectionsOnce(mountId); } while (sectionsRefreshQueued && dashboardMounted && mountId === dashboardMountId); return success; })(); sectionsRefreshPromise = promise; try { return await promise; } finally { if (sectionsRefreshPromise === promise) { sectionsRefreshPromise = null; } } } function setSubscriptionUpdating(sectionName, updating, local = false) { if (local || !updating) { setLocalSubscriptionAction(sectionName, updating && local); } const sectionsWidget = store.get().sectionsWidget; const subscriptionUpdatingSections = { ...sectionsWidget.subscriptionUpdatingSections }; if (updating) { subscriptionUpdatingSections[sectionName] = true; } else { delete subscriptionUpdatingSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, subscriptionUpdatingSections } }); } function setSelectorSwitching(sectionName, tag) { const sectionsWidget = store.get().sectionsWidget; const selectorSwitchingSections = { ...sectionsWidget.selectorSwitchingSections }; if (tag) { selectorSwitchingSections[sectionName] = tag; } else { delete selectorSwitchingSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, selectorSwitchingSections } }); } function setLatencyFetching(sectionName, fetching, local = false, progress) { if (local || !fetching) { setLocalLatencyAction(sectionName, fetching && local); } const sectionsWidget = store.get().sectionsWidget; const latencyFetchingSections = { ...sectionsWidget.latencyFetchingSections }; const latencyProgressSections = { ...sectionsWidget.latencyProgressSections }; if (fetching) { latencyFetchingSections[sectionName] = true; if (progress) { latencyProgressSections[sectionName] = progress; } } else { delete latencyFetchingSections[sectionName]; delete latencyProgressSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, latencyFetchingSections, latencyProgressSections } }); } async function completeSubscriptionUpdateJob(jobId, sectionName, response) { if (pageUnloading) { setSubscriptionUpdating(sectionName, false); return; } if (jobId && handledSubscriptionJobs.has(jobId)) { setSubscriptionUpdating(sectionName, false); return; } const shouldNotify = jobId ? shouldNotifyOwnedUiAction("subscription", jobId) : false; const failed2 = !response.success || response.data.success === false; const message = response.success ? response.data.message || _("Failed to update subscriptions") : response.error || _("Failed to update subscriptions"); if (failed2 && isTransientRpcError(message)) { void refreshRuntimeUiState({ force: true }); return; } if (jobId) { handledSubscriptionJobs.add(jobId); } setSubscriptionUpdating(sectionName, false); if (jobId && response.success) { void ForkopShellMethods.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 ForkopShellMethods.waitSubscriptionUpdateJob(jobId) : { success: true, data: state }; await completeSubscriptionUpdateJob(jobId, sectionName, response); } catch (error) { logger.error("[DASHBOARD]", "followSubscriptionUpdateState failed", error); if (!pageUnloading) { const message = error instanceof Error ? error.message : _("Failed to update subscriptions"); setSubscriptionUpdating(sectionName, false); if (!isTransientRpcError(message)) { showToast(_("Failed to update subscriptions"), "error"); } } } finally { followedSubscriptionJobs.delete(jobId); } } async function completeLatencyTestJob(jobId, sectionName) { setLatencyFetching(sectionName, false); if (pageUnloading) { return; } if (jobId && handledLatencyJobs.has(jobId)) { return; } if (jobId) { handledLatencyJobs.add(jobId); } if (jobId) { void ForkopShellMethods.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 ForkopShellMethods.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 ForkopShellMethods.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 ForkopShellMethods.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.forkopRunning }); } function stopDashboardDataUpdates() { dashboardDataUpdatesStarted = false; dashboardDataUpdatesId += 1; if (sectionsRefreshTimer) { clearInterval(sectionsRefreshTimer); sectionsRefreshTimer = 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); } function syncDashboardServiceAvailability() { const availability = getDashboardServiceAvailability(); const stopped = availability === "stopped"; const container = document.getElementById("dashboard-status"); container?.classList.toggle("fkp_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] || section.outbounds.some( (outbound) => outbound.code === tag && outbound.selected )) { return; } setSelectorSwitching(sectionName, tag); try { await ForkopShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections({ force: true }); } 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 { const startResponse = await ForkopShellMethods.latencyTestStart( latencyType, sectionName, tag, timeout ); if (!startResponse.success) { throw new Error(startResponse.error); } jobId = startResponse.data.job_id; if (followedLatencyJobs.has(jobId)) { completed = true; return; } followedLatencyJobs.add(jobId); ownsJobFollow = true; await ForkopShellMethods.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); } } } async function handleCopyOutbound(section, outbound) { const link = outbound.link; if (link && isCopyableProxyLink(link)) { copyToClipboard(link); return; } const response = await ForkopShellMethods.getOutboundLink( section.sectionName, outbound.code ); if (response.success && isCopyableProxyLink(response.data.link)) { copyToClipboard(response.data.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 "fkp_dashboard-page__outbound-grid__item__latency--empty"; } if (latency < 800) { return "fkp_dashboard-page__outbound-grid__item__latency--green"; } if (latency < 1500) { return "fkp_dashboard-page__outbound-grid__item__latency--yellow"; } return "fkp_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: "fkp_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 member.displayName; } return [ E( "span", { class: "fkp_dashboard-page__urltest-details__country-badge" }, countryFlag ), 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); } return E( "span", { class: "fkp_dashboard-page__urltest-details__selected-value" }, [ E( "span", { class: "fkp_dashboard-page__urltest-details__selected-name" }, selectedMember ? renderDetailsMemberName(selectedMember) : name ), ...selectedMember?.type ? [ E( "span", { class: "fkp_dashboard-page__urltest-details__selected-type" }, selectedMember.type ) ] : [], ...selectedMember ? [ E( "span", { class: getUrlTestLatencyClass(selectedMember.latency) }, formatUrlTestLatency(selectedMember.latency) ) ] : [] ] ); } function renderUrlTestCopyButton(title, onClick) { return E( "button", { type: "button", class: "btn fkp_dashboard-page__urltest-details__copy-button", title, "aria-label": title, click: onClick }, renderCopyIcon24() ); } function renderUrlTestInfoModal(section, outbound) { 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 existing connections"), value: info.interruptExistConnections } ]; return E("div", { class: "fkp_dashboard-page__urltest-details" }, [ E( "dl", { class: "fkp_dashboard-page__urltest-details__params" }, fields.map( ({ label, value, children }) => E("div", { class: "fkp_dashboard-page__urltest-details__param" }, [ E("dt", {}, label), E( "dd", {}, children || [E("span", {}, formatUrlTestModalValue(value))] ) ]) ) ), E("div", { class: "fkp_dashboard-page__urltest-details__outbounds" }, [ E( "div", { class: "fkp_dashboard-page__urltest-details__outbounds-title" }, _("Nodes") ), E( "div", { class: "fkp_dashboard-page__urltest-details__table" }, info.outbounds.length ? info.outbounds.map( (member) => E( "div", { class: [ "fkp_dashboard-page__urltest-details__row", member.selected ? "fkp_dashboard-page__urltest-details__row--active" : "" ].filter(Boolean).join(" ") }, [ E( "div", { class: "fkp_dashboard-page__urltest-details__row-name" }, [ E("b", {}, renderDetailsMemberName(member)), ...member.type ? [ E( "span", { class: "fkp_dashboard-page__urltest-details__row-type" }, member.type ) ] : [] ] ), E( "div", { class: "fkp_dashboard-page__urltest-details__row-meta" }, [ E( "span", { class: getUrlTestLatencyClass(member.latency) }, formatUrlTestLatency(member.latency) ) ] ), member.canCopyLink ? renderUrlTestCopyButton(_("Copy proxy link"), (event) => { event.preventDefault(); void handleCopyOutbound(section, member); }) : E("span", { class: "fkp_dashboard-page__urltest-details__copy-placeholder" }) ] ) ) : [ E( "div", { class: "fkp_dashboard-page__urltest-details__empty" }, _("Node list is empty") ) ] ) ]), E("div", { class: "fkp_dashboard-page__urltest-details__footer" }, [ E( "button", { type: "button", class: "btn cbi-button cbi-button-neutral", click: () => { ui.hideModal(); } }, _("Close") ) ]) ]); } function handleShowUrlTestInfo(section, outbound) { if (!outbound.urlTestInfo) { return; } ui.showModal( `${_("URLTest details")}: ${outbound.urlTestInfo.displayName || outbound.displayName}`, renderUrlTestInfoModal(section, outbound) ); } 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); } return E( "span", { class: "fkp_dashboard-page__urltest-details__selected-value" }, [ E( "span", { class: [ "fkp_dashboard-page__urltest-details__selected-name", selectedMember ? "fkp_dashboard-page__urltest-details__priority-name" : "" ].filter(Boolean).join(" ") }, selectedMember ? renderPriorityMemberName(selectedMember) : name ), ...selectedMember?.type ? [ E( "span", { class: "fkp_dashboard-page__urltest-details__selected-type" }, selectedMember.type ) ] : [], ...selectedMember ? [ E( "span", { class: getUrlTestLatencyClass(selectedMember.latency) }, formatUrlTestLatency(selectedMember.latency) ) ] : [] ] ); } function renderPriorityMemberName(member) { const levelName = member.levelName || _("Level"); return [ E( "span", { class: "fkp_dashboard-page__urltest-details__priority-number" }, `#${member.levelIndex + 1}` ), E( "span", { class: "fkp_dashboard-page__urltest-details__priority-level" }, levelName ), E( "span", { class: "fkp_dashboard-page__urltest-details__priority-node" }, renderDetailsMemberName(member) ) ]; } function renderPriorityInfoModal(section, outbound) { 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 existing connections"), value: info.interruptExistConnections } ]; return E("div", { class: "fkp_dashboard-page__urltest-details" }, [ E( "dl", { class: "fkp_dashboard-page__urltest-details__params" }, fields.map( ({ label, value, children }) => E("div", { class: "fkp_dashboard-page__urltest-details__param" }, [ E("dt", {}, label), E( "dd", {}, children || [E("span", {}, formatUrlTestModalValue(value))] ) ]) ) ), E("div", { class: "fkp_dashboard-page__urltest-details__outbounds" }, [ E( "div", { class: "fkp_dashboard-page__urltest-details__outbounds-title" }, _("Nodes") ), E( "div", { class: "fkp_dashboard-page__urltest-details__table" }, info.outbounds.length ? info.outbounds.map( (member) => E( "div", { class: [ "fkp_dashboard-page__urltest-details__row", member.selected ? "fkp_dashboard-page__urltest-details__row--active" : "" ].filter(Boolean).join(" ") }, [ E( "div", { class: "fkp_dashboard-page__urltest-details__row-name" }, [ E( "b", { class: "fkp_dashboard-page__urltest-details__priority-name" }, renderPriorityMemberName(member) ), ...member.type ? [ E( "span", { class: "fkp_dashboard-page__urltest-details__row-type" }, member.type ) ] : [] ] ), E( "div", { class: "fkp_dashboard-page__urltest-details__row-meta" }, [ E( "span", { class: getUrlTestLatencyClass(member.latency) }, formatUrlTestLatency(member.latency) ) ] ), member.canCopyLink ? renderUrlTestCopyButton(_("Copy proxy link"), (event) => { event.preventDefault(); void handleCopyOutbound(section, member); }) : E("span", { class: "fkp_dashboard-page__urltest-details__copy-placeholder" }) ] ) ) : [ E( "div", { class: "fkp_dashboard-page__urltest-details__empty" }, _("Node list is empty") ) ] ) ]), E("div", { class: "fkp_dashboard-page__urltest-details__footer" }, [ E( "button", { type: "button", class: "btn cbi-button cbi-button-neutral", click: () => { ui.hideModal(); } }, _("Close") ) ]) ]); } function handleShowPriorityInfo(section, outbound) { if (!outbound.priorityInfo) { return; } ui.showModal( `${_("Priority details")}: ${outbound.priorityInfo.displayName || outbound.displayName}`, renderPriorityInfoModal(section, outbound) ); } 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 ForkopShellMethods.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 ForkopShellMethods.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 text = 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; } if (sectionsWidget.loading || sectionsWidget.failed) { const renderedWidget = renderSections({ loading: sectionsWidget.loading, failed: sectionsWidget.failed, section: { code: "", sectionName: "", displayName: "", outbounds: [], withTagSelect: false }, onTestLatency: () => { }, onChooseOutbound: () => { }, onCopyOutbound: () => { }, onShowUrlTestInfo: () => { }, onShowPriorityInfo: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, latencyProgress: void 0, subscriptionUpdating: false, selectorSwitchingTag: void 0 }); return preserveScrollForPage(() => { container.replaceChildren(renderedWidget); }); } const renderedWidgets = sectionsWidget.data.map( (section) => renderSections({ loading: sectionsWidget.loading, failed: sectionsWidget.failed, section, 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: (section2, outbound) => { void handleCopyOutbound(section2, outbound); }, onShowUrlTestInfo: (section2, outbound) => { handleShowUrlTestInfo(section2, outbound); }, onShowPriorityInfo: (section2, outbound) => { handleShowPriorityInfo(section2, outbound); }, onUpdateSubscription: (section2) => { void handleUpdateSubscription(section2); } }) ); return preserveScrollForPage(() => { container.replaceChildren(...renderedWidgets); }); } async function renderBandwidthWidget() { logger.debug("[DASHBOARD]", "renderBandwidthWidget"); const traffic = store.get().bandwidthWidget; const container = document.getElementById("dashboard-widget-traffic"); if (!container) { return; } if (traffic.loading || traffic.failed) { const renderedWidget2 = renderWidget({ loading: traffic.loading, failed: traffic.failed, title: "", items: [] }); return container.replaceChildren(renderedWidget2); } const renderedWidget = renderWidget({ loading: traffic.loading, failed: traffic.failed, title: _("Traffic"), items: [ { key: _("Uplink"), value: `${prettyBytes(traffic.data.up)}/s` }, { key: _("Downlink"), value: `${prettyBytes(traffic.data.down)}/s` } ] }); container.replaceChildren(renderedWidget); } async function renderTrafficTotalWidget() { logger.debug("[DASHBOARD]", "renderTrafficTotalWidget"); const trafficTotalWidget = store.get().trafficTotalWidget; const container = document.getElementById("dashboard-widget-traffic-total"); if (!container) { return; } if (trafficTotalWidget.loading || trafficTotalWidget.failed) { const renderedWidget2 = renderWidget({ loading: trafficTotalWidget.loading, failed: trafficTotalWidget.failed, title: "", items: [] }); return container.replaceChildren(renderedWidget2); } const renderedWidget = renderWidget({ loading: trafficTotalWidget.loading, failed: trafficTotalWidget.failed, title: _("Traffic Total"), items: [ { key: _("Uplink"), value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)) }, { key: _("Downlink"), value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)) } ] }); container.replaceChildren(renderedWidget); } async function renderSystemInfoWidget() { logger.debug("[DASHBOARD]", "renderSystemInfoWidget"); const systemInfoWidget = store.get().systemInfoWidget; const container = document.getElementById("dashboard-widget-system-info"); if (!container) { return; } if (systemInfoWidget.loading || systemInfoWidget.failed) { const renderedWidget2 = renderWidget({ loading: systemInfoWidget.loading, failed: systemInfoWidget.failed, title: "", items: [] }); return container.replaceChildren(renderedWidget2); } const renderedWidget = renderWidget({ loading: systemInfoWidget.loading, failed: systemInfoWidget.failed, title: _("System info"), items: [ { key: _("Active Connections"), value: String(systemInfoWidget.data.connections) }, { key: _("Memory Usage"), value: String(prettyBytes(systemInfoWidget.data.memory)) } ] }); container.replaceChildren(renderedWidget); } async function renderServicesInfoWidget() { logger.debug("[DASHBOARD]", "renderServicesInfoWidget"); const servicesInfoWidget = store.get().servicesInfoWidget; const container = document.getElementById("dashboard-widget-service-info"); if (!container) { return; } if (servicesInfoWidget.loading || servicesInfoWidget.failed) { const renderedWidget2 = renderWidget({ loading: servicesInfoWidget.loading, failed: servicesInfoWidget.failed, title: "", items: [] }); return container.replaceChildren(renderedWidget2); } const renderedWidget = renderWidget({ loading: servicesInfoWidget.loading, failed: servicesInfoWidget.failed, title: _("Services info"), items: [ { key: "Forkop", value: servicesInfoWidget.data.forkopRunning ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: servicesInfoWidget.data.forkopRunning ? "fkp_dashboard-page__widgets-section__item__row--success" : "fkp_dashboard-page__widgets-section__item__row--error" } }, { key: "Sing-box", value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: servicesInfoWidget.data.singbox ? "fkp_dashboard-page__widgets-section__item__row--success" : "fkp_dashboard-page__widgets-section__item__row--error" } } ] }); container.replaceChildren(renderedWidget); } 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 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/forkop/tabs/dashboard/styles.ts var styles = ` #cbi-${FORKOP_UCI_PACKAGE}-dashboard-_mount_node > .cbi-value-title { display: none; } #cbi-${FORKOP_UCI_PACKAGE}-dashboard-_mount_node > .cbi-value-field { margin-left: 0; width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-dashboard-_mount_node > div { width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-dashboard > h3 { display: none; } .fkp_dashboard-page { width: 100%; --dashboard-grid-columns: 4; --dashboard-grid-min-width: 180px; } .fkp_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; } .fkp_dashboard-page--service-stopped { display: grid; grid-template-columns: repeat(var(--dashboard-grid-columns), minmax(var(--dashboard-grid-min-width), 1fr)); gap: 10px; } .fkp_dashboard-page--service-stopped .fkp_dashboard-page__service-stopped { display: flex; grid-column: 1 / -1; } .fkp_dashboard-page--service-stopped .fkp_dashboard-page__content { display: none; } @media (max-width: 900px) { .fkp_dashboard-page { --dashboard-grid-columns: 2; } } @media (max-width: 560px) { .fkp_dashboard-page { --dashboard-grid-columns: 1; --dashboard-grid-min-width: 0; } } .fkp_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; } .fkp_dashboard-page__widgets-section__item { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; min-width: 0; } .fkp_dashboard-page__widgets-section__item__title {} .fkp_dashboard-page__widgets-section__item__row {} .fkp_dashboard-page__widgets-section__item__row--success .fkp_dashboard-page__widgets-section__item__row__value { color: var(--success-color-medium, green); } .fkp_dashboard-page__widgets-section__item__row--error .fkp_dashboard-page__widgets-section__item__row__value { color: var(--error-color-medium, red); } .fkp_dashboard-page__widgets-section__item__row__key {} .fkp_dashboard-page__widgets-section__item__row__value {} .fkp_dashboard-page__outbound-section { margin-top: 10px; border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; } .fkp_dashboard-page__outbound-section__title-section { display: flex; align-items: center; justify-content: space-between; gap: 8px 10px; min-width: 0; } .fkp_dashboard-page__outbound-section__title-section__title { color: var(--text-color-high); font-weight: 700; min-width: 0; overflow-wrap: anywhere; } .fkp_dashboard-page__outbound-section__title-section__actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex: 0 0 auto; } .fkp_dashboard-page .btn.fkp_dashboard-page__outbound-section__subscription-update { min-width: 130px; min-height: 28px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; } .fkp_dashboard-page__outbound-section__subscription-update svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .fkp_dashboard-page__outbound-section__subscription-update[disabled] { cursor: not-allowed; opacity: 0.65; } .fkp_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; } .fkp_dashboard-page .btn.dashboard-sections-grid-item-test-latency svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .fkp_dashboard-page .btn.dashboard-sections-grid-item-test-latency[disabled] { cursor: not-allowed; opacity: 0.65; } .fkp_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; } .fkp_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); } .fkp_dashboard-page__subscription-meta__main { display: flex; align-items: center; gap: 6px 10px; min-width: 0; } .fkp_dashboard-page__subscription-meta__heading { flex: 0 0 auto; color: var(--text-color-high); font-weight: 700; line-height: 1.25; white-space: nowrap; } .fkp_dashboard-page__subscription-meta__title { flex: 0 1 auto; width: max-content; max-width: min(28ch, 30%); min-width: min-content; color: var(--text-color-high); font-weight: 700; line-height: 1.25; overflow-wrap: anywhere; } .fkp_dashboard-page__subscription-meta__facts { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 5px 12px; } .fkp_dashboard-page__subscription-meta__fact { display: flex; align-items: baseline; gap: 4px; min-width: 0; line-height: 1.25; } .fkp_dashboard-page__subscription-meta__fact-key { color: var(--text-color-medium); font-size: 12px; white-space: nowrap; } .fkp_dashboard-page__subscription-meta__fact-value { color: var(--text-color-high); font-weight: 600; overflow-wrap: anywhere; } .fkp_dashboard-page__subscription-meta__actions { flex: 0 0 auto; margin-left: auto; display: flex; justify-content: flex-end; gap: var(--subscription-meta-action-gap); } .fkp_dashboard-page .btn.fkp_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; } .fkp_dashboard-page__subscription-meta__action svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .fkp_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) { .fkp_dashboard-page__subscription-meta__main { align-items: flex-start; flex-wrap: wrap; } .fkp_dashboard-page__subscription-meta__heading, .fkp_dashboard-page__subscription-meta__title { order: 1; } .fkp_dashboard-page__subscription-meta__actions { order: 2; } .fkp_dashboard-page__subscription-meta__facts { order: 3; flex-basis: 100%; } .fkp_dashboard-page__subscription-meta__title { max-width: calc(100% - 42px); } } .fkp_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; } .fkp_dashboard-page__outbound-grid__item--selectable { cursor: pointer; } .fkp_dashboard-page__outbound-grid__item--selectable:hover { border-color: var(--primary-color-high, dodgerblue); } .fkp_dashboard-page__outbound-grid__item--active { border-color: var(--success-color-medium, green); } .fkp_dashboard-page__outbound-grid__item--disabled { cursor: default; } .fkp_dashboard-page__outbound-grid__item--switching { border-color: transparent !important; overflow: hidden; cursor: wait; } .fkp_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; } .fkp_dashboard-page__outbound-grid__item__snake rect { stroke: var(--primary-color-high, dodgerblue); stroke-width: 4; animation: fkp-dashboard-selector-snake-svg 1.2s linear infinite; } @keyframes fkp-dashboard-selector-snake-svg { 0% { stroke-dasharray: 30 70; stroke-dashoffset: 100; } 100% { stroke-dasharray: 30 70; stroke-dashoffset: 0; } } .fkp_dashboard-page__outbound-grid__item__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; } .fkp_dashboard-page__outbound-grid__item__header b { min-width: 0; line-height: 1.25; overflow-wrap: anywhere; } .fkp_dashboard-page .btn.fkp_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; } .fkp_dashboard-page__outbound-grid__item__copy-button svg { width: 13px; height: 13px; display: block; flex: 0 0 auto; } .fkp_dashboard-page__outbound-grid__item__footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; } .fkp_dashboard-page__outbound-grid__item__type { min-width: 0; overflow-wrap: anywhere; } .fkp_dashboard-page__outbound-grid__item__latency--empty { color: var(--primary-color-low, lightgray); } .fkp_dashboard-page__outbound-grid__item__latency--green { color: var(--success-color-medium, green); } .fkp_dashboard-page__outbound-grid__item__latency--yellow { color: var(--warn-color-medium, orange); } .fkp_dashboard-page__outbound-grid__item__latency--red { color: var(--error-color-medium, red); } .fkp_dashboard-page__urltest-details { box-sizing: border-box; width: min(760px, calc(100vw - 56px)); max-width: 100%; padding-top: 10px; } .fkp_dashboard-page__urltest-details__params { display: grid; grid-template-columns: minmax(120px, max-content) minmax(0, 1fr); gap: 8px 16px; margin: 0 0 18px; } .fkp_dashboard-page__urltest-details__param { display: contents; } .fkp_dashboard-page__urltest-details__param dt { color: var(--text-color-medium, #666); line-height: 1.35; } .fkp_dashboard-page__urltest-details__param dd { display: flex; align-items: center; gap: 8px; min-width: 0; margin: 0; } .fkp_dashboard-page__urltest-details__param dd span { min-width: 0; overflow-wrap: anywhere; } .fkp_dashboard-page__urltest-details__url { min-width: 0; color: var(--primary-color-high, #337ab7); text-decoration: none; overflow-wrap: anywhere; } .fkp_dashboard-page__urltest-details__url:hover { text-decoration: underline; } .fkp_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; } .fkp_dashboard-page__urltest-details__selected-name { min-width: 0; font-weight: 600; overflow-wrap: anywhere; } .fkp_dashboard-page__urltest-details__selected-type { color: var(--text-color-medium, #666); } .fkp_dashboard-page__urltest-details__outbounds-title { margin-bottom: 8px; font-weight: 600; } .fkp_dashboard-page__urltest-details__table { display: grid; gap: 6px; width: calc(100% + 14px); box-sizing: border-box; max-height: min(46vh, 460px); overflow-x: hidden; overflow-y: auto; padding-right: 14px; scrollbar-gutter: auto; } .fkp_dashboard-page__urltest-details__row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(54px, max-content) 20px; align-items: center; gap: 8px; width: 100%; min-width: 0; padding: 7px 8px; box-sizing: border-box; border: 1px solid transparent; border-bottom: 1px solid var(--border-color-low, #eee); border-radius: 4px; } .fkp_dashboard-page__urltest-details__row--active { border-color: var(--success-color-low, #2d7d46); background: transparent; } .fkp_dashboard-page__urltest-details__row-name, .fkp_dashboard-page__urltest-details__row-meta { display: flex; align-items: center; gap: 6px; min-width: 0; line-height: 1.3; } .fkp_dashboard-page__urltest-details__row-name { flex-wrap: wrap; } .fkp_dashboard-page__urltest-details__row-name b { min-width: 0; overflow-wrap: anywhere; line-height: 1.3; } .fkp_dashboard-page__urltest-details__priority-name { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 2px 0; } .fkp_dashboard-page__urltest-details__priority-number { margin-right: 6px; color: var(--text-color-medium, #aaa); font-family: monospace; font-size: 13px; font-weight: 600; } .fkp_dashboard-page__urltest-details__priority-level { margin-right: 8px; padding: 2px 6px; border-radius: 4px; color: var(--text-color-medium, #aaa); background: rgba(128, 128, 128, 0.15); font-size: 11px; font-weight: 400; } .fkp_dashboard-page__urltest-details__country-badge { display: inline-flex; align-items: center; user-select: none; margin-right: 6px; 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; } .fkp_dashboard-page__urltest-details__priority-node { color: var(--text-color-high, #fff); font-weight: 600; } .fkp_dashboard-page__urltest-details__row-type, .fkp_dashboard-page__urltest-details__row-meta { color: var(--text-color-medium, #666); } .fkp_dashboard-page__urltest-details__row-type { white-space: nowrap; line-height: 1.3; } .fkp_dashboard-page__urltest-details__row-meta { justify-content: flex-end; white-space: nowrap; } .fkp_dashboard-page__urltest-details__copy-button { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 20px; width: 20px; min-width: 20px; height: 20px; padding: 0; box-sizing: border-box; } .fkp_dashboard-page__urltest-details__copy-button svg { width: 12px; height: 12px; } .fkp_dashboard-page__urltest-details__copy-placeholder { display: block; width: 20px; min-width: 20px; height: 1px; } .fkp_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; } .fkp_dashboard-page__urltest-details__footer { display: flex; justify-content: flex-end; margin-top: 14px; } @media (max-width: 560px) { .fkp_dashboard-page__urltest-details__params { grid-template-columns: 1fr; } .fkp_dashboard-page__urltest-details__row { grid-template-columns: minmax(0, 1fr) 20px; } .fkp_dashboard-page__urltest-details__row-meta { grid-column: 1 / -1; justify-content: flex-start; } } `; // src/forkop/tabs/dashboard/index.ts var DashboardTab = { render, initController, styles }; // src/forkop/tabs/diagnostic/renderDiagnostic.ts function render2() { return E("div", { id: "diagnostic-status", class: "fkp_diagnostic-page" }, [ E("div", { class: "fkp_diagnostic-page__left-bar" }, [ E("div", { id: "fkp_diagnostic-page-run-check" }), E("div", { class: "fkp_diagnostic-page__checks", id: "fkp_diagnostic-page-checks" }) ]), E("div", { class: "fkp_diagnostic-page__right-bar" }, [ E("div", { id: "fkp_diagnostic-page-wiki" }), E("div", { id: "fkp_diagnostic-page-actions" }), E("div", { id: "fkp_diagnostic-page-system-info" }) ]) ]); } // src/forkop/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/forkop/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/forkop/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/forkop/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 ForkopShellMethods.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/forkop/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 ForkopShellMethods.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/forkop/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 ForkopShellMethods.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/forkop/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 ForkopShellMethods.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" : "error", 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/forkop/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 ForkopShellMethods.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/forkop/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/forkop/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 ForkopShellMethods.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 forkopRuntimeReady = !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 || !forkopRuntimeReady ? "error" : "success", key: hasZapretRules ? forkopRuntimeReady ? _("Forkop-managed nfqws runtime is ready") : _("Forkop-managed nfqws runtime is not ready") : unexpectedRuntime ? _("Unexpected Forkop-managed nfqws runtime is running") : _("Forkop-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 Forkop 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/forkop/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 ForkopShellMethods.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 forkopRuntimeReady = !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 || !forkopRuntimeReady ? "error" : "success", key: hasZapret2Rules ? forkopRuntimeReady ? _("Forkop-managed nfqws2 runtime is ready") : _("Forkop-managed nfqws2 runtime is not ready") : unexpectedRuntime ? _("Unexpected Forkop-managed nfqws2 runtime is running") : _("Forkop-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 Forkop 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/forkop/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 ForkopShellMethods.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 forkopRuntimeReady = !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 || !forkopRuntimeReady ? "error" : runtimeUnstable ? "warning" : "success", key: hasByedpiRules ? runtimeUnstable ? _("Forkop-managed ciadpi runtime has restarted") : forkopRuntimeReady ? _("Forkop-managed ciadpi runtime is ready") : _("Forkop-managed ciadpi runtime is not ready") : unexpectedRuntime ? _("Unexpected Forkop-managed ciadpi runtime is running") : _("Forkop-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 Forkop 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/forkop/services/systemInfo.service.ts var UNKNOWN_SYSTEM_INFO = { loading: false, loaded: false, providerInfoLoaded: false, forkop_version: _("unknown"), forkop_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_tailscale: 1, 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 ForkopShellMethods.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 = ` .fkp-partial-button { text-align: center; } .fkp-partial-button--with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 5px; } .fkp-partial-button--loading { } .fkp-partial-button--disabled { } .fkp-partial-button__icon { flex: 0 0 auto; } .fkp-partial-button__icon { display: inline-flex; align-items: center; justify-content: center; } .fkp-partial-button__icon svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } `; // src/partials/modal/styles.ts var styles3 = ` .fkp-partial-modal__body {} .fkp-partial-modal__content { max-height: 70vh; overflow: scroll; border-radius: 4px; } .fkp-partial-modal__footer { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 10px; } .fkp-partial-modal__footer button { margin-left: 0; } .fkp-partial-modal__checkbox { display: inline-flex; align-items: center; gap: 8px; margin-right: auto; cursor: pointer; user-select: none; } .fkp-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: "fkp-partial-button__icon" }); if (loading2) { iconWrap.appendChild(renderLoaderCircleIcon24()); return iconWrap; } if (icon) { iconWrap.appendChild(icon()); return iconWrap; } return iconWrap; } function getClass() { return [ "btn", "fkp-partial-button", ...insertIf(Boolean(disabled), ["fkp-partial-button--disabled"]), ...insertIf(Boolean(loading2), ["fkp-partial-button--loading"]), ...insertIf(Boolean(hasIcon), ["fkp-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: "fkp-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: "fkp-partial-modal__checkbox" }, [ autoRefreshInput, E( "span", { class: "fkp-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: "fkp-partial-modal__checkbox" }, [ maskValuesInput, E( "span", { class: "fkp-partial-modal__checkbox-text" }, options.maskValuesLabel ?? _("Hide values") ) ]) ); } const body = E("div", { class: "fkp-partial-modal__body" }, [ E("div", {}, [ contentEl, E("div", { class: "fkp-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/forkop/tabs/diagnostic/partials/renderAvailableActions.ts function renderAvailableActions({ restart, start, stop, enable, disable, globalCheck, viewLogs, showSingBoxConfig }) { return E("div", { class: "fkp_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), ...insertIf(restart.visible, [ renderButton({ classNames: ["cbi-button-apply"], onClick: restart.onClick, icon: renderRotateCcwIcon24, text: _("Restart Forkop"), loading: restart.loading, disabled: restart.disabled }) ]), ...insertIf(stop.visible, [ renderButton({ classNames: ["cbi-button-remove"], onClick: stop.onClick, icon: renderCircleStopIcon24, text: _("Stop Forkop"), loading: stop.loading, disabled: stop.disabled }) ]), ...insertIf(start.visible, [ renderButton({ classNames: ["cbi-button-save"], onClick: start.onClick, icon: renderCirclePlayIcon24, text: _("Start Forkop"), 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(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 }) ]) ]); } // src/forkop/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: "fkp_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: `fkp_diagnostic_alert__summary__item fkp_diagnostic_alert__summary__item--${item.state}` }, [getIcon(), E("b", {}, item.key), E("div", {}, item.value)] ); }); return E("div", { class: "fkp_diagnostic_alert__summary" }, renderedItems); } function renderLoadingState3(props) { const iconWrap = E("span", { class: "fkp_diagnostic_alert__icon" }); iconWrap.appendChild(renderLoaderCircleIcon24()); return E( "div", { class: "fkp_diagnostic_alert fkp_diagnostic_alert--loading" }, [ iconWrap, E("div", { class: "fkp_diagnostic_alert__content" }, [ E("b", { class: "fkp_diagnostic_alert__title" }, props.title), E( "div", { class: "fkp_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderWarningState(props) { const iconWrap = E("span", { class: "fkp_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleAlertIcon24()); return E( "div", { class: "fkp_diagnostic_alert fkp_diagnostic_alert--warning" }, [ iconWrap, E("div", { class: "fkp_diagnostic_alert__content" }, [ E("b", { class: "fkp_diagnostic_alert__title" }, props.title), E( "div", { class: "fkp_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderErrorState(props) { const iconWrap = E("span", { class: "fkp_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleXIcon24()); return E( "div", { class: "fkp_diagnostic_alert fkp_diagnostic_alert--error" }, [ iconWrap, E("div", { class: "fkp_diagnostic_alert__content" }, [ E("b", { class: "fkp_diagnostic_alert__title" }, props.title), E( "div", { class: "fkp_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSuccessState(props) { const iconWrap = E("span", { class: "fkp_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleCheckIcon24()); return E( "div", { class: "fkp_diagnostic_alert fkp_diagnostic_alert--success" }, [ iconWrap, E("div", { class: "fkp_diagnostic_alert__content" }, [ E("b", { class: "fkp_diagnostic_alert__title" }, props.title), E( "div", { class: "fkp_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSkippedState(props) { const iconWrap = E("span", { class: "fkp_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleSlashIcon24()); return E( "div", { class: "fkp_diagnostic_alert fkp_diagnostic_alert--skipped" }, [ iconWrap, E("div", { class: "fkp_diagnostic_alert__content" }, [ E("b", { class: "fkp_diagnostic_alert__title" }, props.title), E( "div", { class: "fkp_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/forkop/tabs/diagnostic/partials/renderRunAction.ts function renderRunAction({ loading: loading2, disabled, click }) { return E("div", { class: "fkp_diagnostic-page__run_check_wrapper" }, [ renderButton({ text: _("Run Diagnostic"), onClick: click, icon: renderSearchIcon24, loading: loading2, disabled, classNames: ["cbi-button-apply"] }) ]); } // src/forkop/tabs/diagnostic/partials/renderSystemInfo.ts function renderSystemInfo({ items }) { return E("div", { class: "fkp_diagnostic-page__right-bar__system-info" }, [ E( "b", { class: "fkp_diagnostic-page__right-bar__system-info__title" }, _("System information") ), ...items.map((item) => { const tagClass = [ "fkp_diagnostic-page__right-bar__system-info__row__tag", ...insertIf(item.tag?.kind === "neutral", [ "fkp_diagnostic-page__right-bar__system-info__row__tag--neutral" ]), ...insertIf(item.tag?.kind === "warning", [ "fkp_diagnostic-page__right-bar__system-info__row__tag--warning" ]), ...insertIf(item.tag?.kind === "success", [ "fkp_diagnostic-page__right-bar__system-info__row__tag--success" ]) ].filter(Boolean).join(" "); return E( "div", { class: "fkp_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) { if (version.includes("COMPILED")) { return "dev"; } return version; } // src/forkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts function renderWikiDisclaimer(kind) { const iconWrap = E("span", { class: "fkp_diagnostic-page__right-bar__wiki__icon" }); iconWrap.appendChild(renderBookOpenTextIcon24()); const className = [ "fkp_diagnostic-page__right-bar__wiki", ...insertIf(kind === "error", [ "fkp_diagnostic-page__right-bar__wiki--error" ]), ...insertIf(kind === "warning", [ "fkp_diagnostic-page__right-bar__wiki--warning" ]) ].join(" "); return E("div", { class: className }, [ E("div", { class: "fkp_diagnostic-page__right-bar__wiki__content" }, [ iconWrap, E("div", { class: "fkp_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/forkop/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 ForkopShellMethods.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 ForkopShellMethods.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") }; } const selectedOutbound = section.outbounds[0]; const latencyProxy = await ForkopShellMethods.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/forkop/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, forkopRunning, mutatingServiceActionLoading }) { return !providerInfoLoaded || servicesInfoLoading || !forkopRunning || 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({ forkopRunning, restartLoading, startLoading, stopLoading }) { return restartLoading || forkopRunning && !startLoading && !stopLoading; } function shouldShowStartAction({ forkopRunning, restartLoading, startLoading, stopLoading }) { return startLoading || !restartLoading && !forkopRunning && !stopLoading; } function shouldShowStopAction({ forkopRunning, restartLoading, startLoading, stopLoading }) { return stopLoading || restartLoading || forkopRunning && !startLoading; } // src/forkop/tabs/diagnostic/diagnosticRunPersistence.ts var DIAGNOSTIC_RUN_STORAGE_KEY = "forkop: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/forkop/tabs/diagnostic/helpers/maskDiagnostics.ts var MASKED_VALUE = "MASKED"; 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 FORKOP_MASK_AFTER_TOKEN = [ "option proxy_string", "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 FORKOP_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", "option dns_server", "option bootstrap_dns_server", "list dns_server", "list bootstrap_dns_server", "option listen", "option listen_port", "option public_host", "option mtproto_faketls", "option mtproto_domain_fronting_ip", "option tls_server_name", "option reality_handshake_server", "option reality_handshake_server_port", "option transport_host", "list transport_hosts", "option tailscale_auth_key", "option tailscale_control_url", "option tailscale_hostname", "list tailscale_advertise_routes", "option tailscale_ephemeral", "option tailscale_exit_node", "option tailscale_exit_node_allow_lan_access", "option mixed_proxy_username", "option mixed_proxy_password", "option ipaddr", "option netmask", "option gateway", "option username", "option password" ]; 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)}/MASKED'${line.slice(quote + 1)}`; } function maskGlobalCheckLine(line) { let maskedLine = line; for (const token of FORKOP_MASK_AFTER_TOKEN) { maskedLine = maskAfterToken(maskedLine, token); } for (const token of FORKOP_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/forkop/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.forkopStatus); } function getForkopStatusText(running, enabled) { if (running) { return enabled ? "running & enabled" : "running but disabled"; } return enabled ? "stopped but enabled" : "stopped & disabled"; } function setDisplayedForkopRunning(running) { const servicesInfoWidget = store.get().servicesInfoWidget; const enabled = Boolean(servicesInfoWidget.data.forkopEnabled); store.set({ servicesInfoWidget: { ...servicesInfoWidget, loading: false, data: { ...servicesInfoWidget.data, forkopRunning: running ? 1 : 0, forkopStatus: getForkopStatusText(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 waitForForkopRunningState(expectedRunning) { const startedAt = Date.now(); while (Date.now() - startedAt < SERVICE_ACTION_STATUS_TIMEOUT_MS) { await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); const forkopRunning = Boolean( store.get().servicesInfoWidget.data.forkopRunning ); if (forkopRunning === 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 ForkopShellMethods.waitServiceActionJob(jobId); } } catch (error) { logger.error("[DIAGNOSTIC]", "followServiceActionState failed", error); } finally { handledServiceActionJobs.add(jobId); setServiceActionStateLoading(state, false); await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); void ForkopShellMethods.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_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([ ForkopShellMethods.checkZapretRuntime(), ForkopShellMethods.checkZapret2Runtime(), ForkopShellMethods.checkByedpiRuntime(), ForkopShellMethods.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("fkp_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 forkopRunning = Boolean(servicesInfoWidget.data.forkopRunning); const container = document.getElementById("fkp_diagnostic-page-run-check"); const renderedAction = renderRunAction({ loading: loading2, disabled: shouldDisableDiagnosticRunAction({ providerInfoLoaded, servicesInfoLoading: servicesInfoWidget.loading, forkopRunning, 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) { setDisplayedForkopRunning(optimisticRunning); } try { const startResponse = await ForkopShellMethods.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 ForkopShellMethods.waitServiceActionJob(jobId); if (!result.success) { throw new Error(result.error); } if (result.data.success === false) { throw new Error(result.data.message || _("Service action failed")); } await waitForForkopRunningState(expectedRunning); } catch (e) { logger.error("[DIAGNOSTIC]", `handleServiceRuntimeAction(${action})`, e); } finally { if (!delegatedToWatcher) { if (ownsJobFollow) { followedServiceActionJobs.delete(jobId); } setDiagnosticActionLoading(action, false); await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); if (jobId) { handledServiceActionJobs.add(jobId); void ForkopShellMethods.uiActionAck("service", jobId); } resetDiagnosticsChecks(); } } } 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 ForkopShellMethods.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 ForkopShellMethods.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 ForkopShellMethods.globalCheck(false); if (globalCheck.success) { const rawGlobalCheckText = globalCheck.data ?? ""; const maskedGlobalCheckText = maskGlobalCheckText(rawGlobalCheckText); ui.showModal( _("Global check"), renderModal(rawGlobalCheckText, "global_check", { maskText: () => maskedGlobalCheckText, initialAutoRefresh: false, showMaskValuesToggle: true }) ); } else { logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", e); } finally { setDiagnosticActionLoading("globalCheck", false); } } async function handleViewLogs() { setDiagnosticActionLoading("viewLogs", true); try { const viewLogs = await ForkopShellMethods.checkLogs(); if (viewLogs.success) { const getLatestLogs = async () => { const latestLogs = await ForkopShellMethods.checkLogs(); if (!latestLogs.success) { throw latestLogs; } return latestLogs.data ?? ""; }; ui.showModal( _("View logs"), renderModal(viewLogs.data, "view_logs", { getText: getLatestLogs, refreshMs: 250, initialAutoRefresh: true, showAutoRefreshToggle: true, startAtEnd: true }) ); } else { logger.error("[DIAGNOSTIC]", "handleViewLogs - e", viewLogs); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleViewLogs - e", e); } finally { setDiagnosticActionLoading("viewLogs", false); } } async function handleShowSingBoxConfig() { setDiagnosticActionLoading("showSingBoxConfig", true); try { const showSingBoxConfig = await ForkopShellMethods.showSingBoxConfig(false); if (showSingBoxConfig.success) { const rawSingBoxConfigText = stringifySingBoxConfig( showSingBoxConfig.data ); const maskedSingBoxConfigText = formatMaskedSingBoxConfig( showSingBoxConfig.data ); ui.showModal( _("Show sing-box config"), renderModal(rawSingBoxConfigText, "show_sing_box_config", { maskText: () => maskedSingBoxConfigText, initialAutoRefresh: false, showMaskValuesToggle: true }) ); } else { logger.error( "[DIAGNOSTIC]", "handleShowSingBoxConfig - e", showSingBoxConfig ); } } catch (e) { logger.error("[DIAGNOSTIC]", "handleShowSingBoxConfig - e", e); } finally { setDiagnosticActionLoading("showSingBoxConfig", false); } } 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("fkp_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 forkopEnabled = Boolean(servicesInfoWidget.data.forkopEnabled); const forkopRunning = Boolean(servicesInfoWidget.data.forkopRunning); const serviceTransition = getServiceTransition( servicesInfoWidget.data.forkopStatus ); 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({ forkopRunning, restartLoading, startLoading, stopLoading }); const stopVisible = shouldShowStopAction({ forkopRunning, restartLoading, startLoading, stopLoading }); const container = document.getElementById("fkp_diagnostic-page-actions"); const renderedActions = renderAvailableActions({ restart: { loading: restartLoading, visible: shouldShowRestartAction({ forkopRunning, 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: !forkopEnabled, onClick: handleEnable, disabled: serviceControlsDisabled }, disable: { loading: diagnosticsActions.disable.loading, visible: forkopEnabled, onClick: handleDisable, disabled: serviceControlsDisabled }, globalCheck: { loading: diagnosticsActions.globalCheck.loading, visible: true, onClick: handleShowGlobalCheck, disabled: utilityActionsDisabled }, viewLogs: { loading: diagnosticsActions.viewLogs.loading, visible: true, onClick: handleViewLogs, disabled: viewLogsDisabled }, showSingBoxConfig: { loading: diagnosticsActions.showSingBoxConfig.loading, visible: true, onClick: handleShowSingBoxConfig, disabled: utilityActionsDisabled } }); return preserveScrollForPage(() => { container.replaceChildren(renderedActions); }); } function renderDiagnosticSystemInfoWidget() { logger.debug("[DIAGNOSTIC]", "renderDiagnosticSystemInfoWidget"); const diagnosticsSystemInfo = store.get().diagnosticsSystemInfo; const container = document.getElementById("fkp_diagnostic-page-system-info"); const items = [ { key: "Forkop", value: normalizeCompiledVersion(diagnosticsSystemInfo.forkop_version) }, { key: "Luci App", value: normalizeCompiledVersion(FORKOP_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/forkop/tabs/diagnostic/styles.ts var styles4 = ` #cbi-${FORKOP_UCI_PACKAGE}-diagnostic-_mount_node > div { width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-diagnostic > h3 { display: none; } .fkp_diagnostic-page { display: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 10px; align-items: start; } @media (max-width: 800px) { .fkp_diagnostic-page { grid-template-columns: 1fr; } } .fkp_diagnostic-page__right-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .fkp_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; } .fkp_diagnostic-page__right-bar__wiki--warning { border: 2px var(--warn-color-medium, orange) solid; } .fkp_diagnostic-page__right-bar__wiki--error { border: 2px var(--error-color-medium, red) solid; } .fkp_diagnostic-page__right-bar__wiki__content { display: grid; grid-template-columns: 1fr 5fr; grid-column-gap: 10px; } .fkp_diagnostic-page__right-bar__wiki__texts {} .fkp_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; } .fkp_diagnostic-page__right-bar__actions > .fkp-partial-button { width: 100%; min-width: 0; margin-left: 0; } .fkp_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; } .fkp_diagnostic-page__right-bar__system-info__title { } .fkp_diagnostic-page__right-bar__system-info__row { display: grid; grid-template-columns: auto 1fr; grid-column-gap: 5px; } .fkp_diagnostic-page__right-bar__system-info__row__tag { padding: 2px 4px; border: 1px transparent solid; border-radius: 4px; margin-left: 5px; } .fkp_diagnostic-page__right-bar__system-info__row__tag--neutral { border: 1px var(--background-color-high, gray) solid; color: var(--text-color-medium, gray); } .fkp_diagnostic-page__right-bar__system-info__row__tag--warning { border: 1px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .fkp_diagnostic-page__right-bar__system-info__row__tag--success { border: 1px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .fkp_diagnostic-page__left-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .fkp_diagnostic-page__run_check_wrapper {} .fkp_diagnostic-page__run_check_wrapper button { width: 100%; } .fkp_diagnostic-page__checks { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .fkp_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; } .fkp_diagnostic_alert--loading { border: 2px var(--primary-color-high, dodgerblue) solid; } .fkp_diagnostic_alert--warning { border: 2px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .fkp_diagnostic_alert--error { border: 2px var(--error-color-medium, red) solid; color: var(--error-color-medium, red); } .fkp_diagnostic_alert--success { border: 2px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .fkp_diagnostic_alert--skipped {} .fkp_diagnostic_alert__icon {} .fkp_diagnostic_alert__content {} .fkp_diagnostic_alert__title { display: block; } .fkp_diagnostic_alert__description {} .fkp_diagnostic_alert__summary { margin-top: 10px; } .fkp_diagnostic_alert__summary__item { display: grid; grid-template-columns: 16px auto 1fr; grid-column-gap: 10px; } .fkp_diagnostic_alert__summary__item--error { color: var(--error-color-medium, red); } .fkp_diagnostic_alert__summary__item--warning { color: var(--warn-color-medium, orange); } .fkp_diagnostic_alert__summary__item--success { color: var(--success-color-medium, green); } .fkp_diagnostic_alert__summary__item__icon { width: 16px; height: 16px; } `; // src/forkop/tabs/diagnostic/index.ts var DiagnosticTab = { render: render2, initController: initController2, styles: styles4 }; // src/forkop/tabs/monitoring/render.ts function render3() { return E( "div", { id: "monitoring-status", class: "fkp_monitoring-page" }, [ E("div", { class: "fkp_monitoring-page__panel" }, [ E("div", { class: "fkp_monitoring-page__controls" }, [ E("div", { class: "fkp_monitoring-page__tabs" }, [ E( "button", { id: "monitoring-tab-active", class: "btn cbi-button fkp_monitoring-page__tab fkp_monitoring-page__tab--active", type: "button" }, `${_("Active")} 0` ), E( "button", { id: "monitoring-tab-closed", class: "btn cbi-button fkp_monitoring-page__tab", type: "button" }, `${_("Closed")} 0` ) ]), E("div", { class: "fkp_monitoring-page__filters" }, [ E( "select", { id: "monitoring-device-filter", class: "cbi-input-select fkp_monitoring-page__device-filter" }, [E("option", { value: "all" }, _("All"))] ), E("label", { class: "fkp_monitoring-page__search" }, [ E("span", { class: "fkp_monitoring-page__search-icon" }, []), E("input", { id: "monitoring-search", class: "cbi-input-text fkp_monitoring-page__search-input", type: "search", placeholder: _("Search"), autocomplete: "off" }) ]) ]), E("div", { class: "fkp_monitoring-page__actions" }, [ E( "button", { id: "monitoring-close-all", class: "btn cbi-button fkp_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 fkp_monitoring-page__icon-button", title: _("Pause updates"), "aria-label": _("Pause updates"), type: "button" }, [] ) ]) ]), E( "div", { id: "monitoring-connections", class: "fkp_monitoring-page__body" }, [ E( "div", { class: "fkp_monitoring-page__state fkp_monitoring-page__state--loading" }, _("Loading connections") ) ] ) ]) ] ); } // src/forkop/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 searchQuery = ""; var localDeviceChoices = {}; var routeDisplayNames = {}; var routeSections = []; var serverDisplayNames = {}; var lastDeviceFilterSignature = ""; var loading = true; var failed = false; var closingAll = false; var monitoringPaused = false; var monitoringPausedAt = null; var serviceAvailability = "loading"; var activeConnections = /* @__PURE__ */ new Map(); var closedConnections = /* @__PURE__ */ new Map(); var closingConnectionIds = /* @__PURE__ */ new Set(); function normalizeString(value) { return value == null ? "" : String(value).trim(); } function getListValues2(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map((item) => normalizeString(item)).filter(Boolean); } return normalizeString(value).split(/\s+/).map((item) => item.trim()).filter(Boolean); } function getUrlTestIds2(section) { const values = getListValues2(section.urltests); return values.length ? values : section.urltest_enabled === "1" ? ["urltest"] : []; } function getUrlTestTag2(sectionName, id) { return getOutboundTagBySection( id === "urltest" ? `${sectionName}-urltest` : `${sectionName}-urltest-${id}` ); } function formatEndpoint(address, port) { const normalizedAddress = normalizeString(address); const normalizedPort = normalizeString(port); if (!normalizedAddress) { return "-"; } if (!normalizedPort) { return normalizedAddress; } if (normalizedPort === "443") { return normalizedAddress; } if (normalizedAddress.includes(":") && !normalizedAddress.startsWith("[")) { return `[${normalizedAddress}]:${normalizedPort}`; } return `${normalizedAddress}:${normalizedPort}`; } function getDisplayName2(section) { return normalizeString(section.label) || section[".name"]; } function buildRouteDisplayNames(sections) { const map = { "bypass-out": "Bypass", "direct-out": "direct" }; const serverMap = {}; const routeSectionItems = []; const urltestsBySection = /* @__PURE__ */ new Map(); sections.filter((section) => section[".type"] === "urltest").forEach((section) => { const owner = normalizeString(section.section); const id = normalizeString(section.id) || section[".name"]; if (!owner || !id) { return; } urltestsBySection.set(owner, [ ...urltestsBySection.get(owner) || [], id ]); }); sections.filter((section) => section[".type"] === "section").filter((section) => section.enabled !== "0").forEach((section) => { const sectionName = section[".name"]; const displayName = getDisplayName2(section); if (!sectionName || !displayName) { return; } routeSectionItems.push({ sectionName, displayName }); map[getOutboundTagBySection(sectionName)] = displayName; const urltestIds = urltestsBySection.get(sectionName) || getUrlTestIds2(section); urltestIds.forEach((id) => { map[getUrlTestTag2(sectionName, id)] = displayName; }); }); sections.filter((section) => section[".type"] === "server").filter((section) => section.enabled !== "0").forEach((section) => { const sectionName = section[".name"]; const displayName = getDisplayName2(section); if (!sectionName || !displayName) { return; } serverMap[`server-${sectionName}-in`] = displayName; }); routeDisplayNames = map; serverDisplayNames = serverMap; routeSections = routeSectionItems.sort( (a, b) => b.sectionName.length - a.sectionName.length ); } function getRouteDisplayNameByTag(tag) { if (!tag) { return ""; } if (routeDisplayNames[tag]) { return routeDisplayNames[tag]; } const manualSection = routeSections.find(({ sectionName }) => { if (!tag.startsWith(`${sectionName}-`) || !tag.endsWith("-out")) { return false; } const middle = tag.slice(sectionName.length + 1, -4); return /^\d+(?:-\d+)?$/.test(middle); }); return manualSection?.displayName || ""; } function getRouteTagFromRule(rule) { const match = normalizeString(rule).match(/=>\s*route\(([^)]+)\)/); return normalizeString(match?.[1]).replace(/^['"]|['"]$/g, ""); } function parseStartedAt(connection) { const startedAt = Date.parse(connection.start || ""); return Number.isFinite(startedAt) ? startedAt : connection.lastSeenAt; } function formatDuration(ms) { const totalSeconds = Math.max(0, Math.floor(ms / 1e3)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor(totalSeconds % 3600 / 60); const seconds = totalSeconds % 60; const pad = (value) => String(value).padStart(2, "0"); if (hours > 0) { return `${hours}:${pad(minutes)}:${pad(seconds)}`; } return `${minutes}:${pad(seconds)}`; } function formatConnectionDuration(connection) { const startedAt = parseStartedAt(connection); const finishedAt = connection.closedAt || monitoringPausedAt || Date.now(); return formatDuration(finishedAt - startedAt); } function formatBytes2(value) { return prettyBytes(Number.isFinite(value) ? Number(value) : 0); } function getConnectionSourceIp(connection) { return normalizeString(connection.metadata?.sourceIP); } function getConnectionInboundTag(connection) { const metadataType = normalizeString(connection.metadata?.type); const metadataTypeParts = metadataType.split("/"); const metadataTag = normalizeString( metadataTypeParts.length > 1 ? metadataTypeParts[metadataTypeParts.length - 1] : metadataType ); if (metadataTag) { return metadataTag; } const ruleInbound = normalizeString(connection.rule).match( /(?:^|\s)inbound=([^\s]+)/ ); return normalizeString(ruleInbound?.[1]); } function getServerDisplayNameByInboundTag(tag) { return normalizeString(serverDisplayNames[tag]); } function getDeviceName(ip) { return normalizeString(localDeviceChoices[ip]); } function getServerSourceNameByIp(ip) { if (!ip) { return ""; } const connections = [ ...Array.from(activeConnections.values()), ...Array.from(closedConnections.values()) ]; for (const connection of connections) { if (getConnectionSourceIp(connection) !== ip) { continue; } const serverName = getServerDisplayNameByInboundTag( getConnectionInboundTag(connection) ); if (serverName) { return serverName; } } return ""; } function getDeviceFilterLabel(ip) { const serverName = getServerSourceNameByIp(ip); if (serverName) { return serverName; } const deviceName = getDeviceName(ip); return deviceName || ip; } function getSourceCellParts(connection) { const ip = getConnectionSourceIp(connection); const inboundTag = getConnectionInboundTag(connection); const serverName = getServerDisplayNameByInboundTag(inboundTag); if (serverName) { return { primary: serverName, ip: "", copyValue: serverName, searchValue: [serverName, ip, inboundTag].filter(Boolean).join(" ") }; } const deviceName = getDeviceName(ip); if (deviceName) { return { primary: deviceName, ip: "", copyValue: deviceName, searchValue: `${deviceName} ${ip}` }; } return { primary: ip || "-", ip: "", copyValue: ip || "-", searchValue: ip }; } function getTargetCellParts(connection) { const metadata = connection.metadata || {}; const host = normalizeString(metadata.host); const destinationIp = normalizeString(metadata.destinationIP); const port = metadata.destinationPort; const primaryTarget = host || destinationIp; const primary = primaryTarget ? formatEndpoint(primaryTarget, port) : "-"; return { primary, searchValue: [primary, host, destinationIp].filter(Boolean).join(" ") }; } function getRoute(connection) { const chains = Array.isArray(connection.chains) ? connection.chains : []; const routeTag = [...chains].reverse().find(getRouteDisplayNameByTag); const fallbackRouteTag = getRouteTagFromRule(connection.rule); const route = getRouteDisplayNameByTag(routeTag || "") || getRouteDisplayNameByTag(fallbackRouteTag) || normalizeString(routeTag) || normalizeString(fallbackRouteTag); return route || "-"; } function getNetwork(connection) { return normalizeString(connection.metadata?.network).toLowerCase() || "-"; } function sortConnections(connections, tab) { return [...connections].sort((a, b) => { if (tab === "closed") { return (b.closedAt || 0) - (a.closedAt || 0); } return parseStartedAt(b) - parseStartedAt(a); }); } function getConnectionsForActiveTab() { const source = activeTab === "active" ? Array.from(activeConnections.values()) : Array.from(closedConnections.values()); return sortConnections(source, activeTab); } function normalizeSearchValue(value) { return value.toLowerCase().replace(/\s+/g, " ").trim(); } function getSearchValues(connection) { const target = getTargetCellParts(connection); const source = getSourceCellParts(connection); return [ connection.id, target.primary, getNetwork(connection), getRoute(connection), formatConnectionDuration(connection), formatBytes2(connection.download), formatBytes2(connection.upload), source.primary, source.copyValue, source.searchValue ].filter(Boolean); } function getVisibleConnections() { const normalizedSearch = normalizeSearchValue(searchQuery); return getConnectionsForActiveTab().filter((connection) => { const sourceIp = getConnectionSourceIp(connection); if (selectedDeviceFilter !== ALL_FILTER_VALUE && sourceIp !== selectedDeviceFilter) { 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(); renderConnections(); } } function setTab(tab) { if (activeTab === tab) { return; } activeTab = tab; renderControls(); renderConnections(); } 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 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("fkp_monitoring-page__tab--active", active); } function renderTabButtonContent(label, count) { return [ E("span", { class: "fkp_monitoring-page__tab-label" }, label), E("span", { class: "fkp_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( "fkp_monitoring-page__icon-button--active", monitoringPaused ); } const searchIcon = document.querySelector( ".fkp_monitoring-page__search-icon" ); if (searchIcon && searchIcon.childNodes.length === 0) { searchIcon.replaceChildren(renderSearchIcon24()); } renderDeviceFilterOptions(); 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: ["fkp_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: "fkp_monitoring-page__value fkp_monitoring-page__source-value fkp_monitoring-page__source-value--ip-only", title: fullText }, source.primary || "-" ); element2.setAttribute("data-copy-value", fullText); return element2; } const element = E( "span", { class: "fkp_monitoring-page__value fkp_monitoring-page__source-value", title: fullText }, [ E("span", { class: "fkp_monitoring-page__source-name" }, source.primary), E("span", { class: "fkp_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 fkp_monitoring-page__row-action", title: _("Close connection"), "aria-label": _("Close connection"), type: "button", value: connection.id, ...isClosing ? { disabled: true } : {} }, [renderXIcon24()] ) : E("span", {}, "-"); return E( "tr", { class: isClosing ? "fkp_monitoring-page__row--closing" : "" }, [ renderTableCell(_("Host"), [renderValue(target.primary)]), renderTableCell(_("Type"), [ renderValue(getNetwork(connection), "fkp_monitoring-page__network") ]), renderTableCell(_("Route"), [ renderValue(getRoute(connection), "fkp_monitoring-page__route") ]), renderTableCell(_("Time"), [ renderValue(formatConnectionDuration(connection)) ]), renderTableCell(_("Downloaded"), [ renderValue(formatBytes2(connection.download)) ]), renderTableCell(_("Uploaded"), [ renderValue(formatBytes2(connection.upload)) ]), renderTableCell(_("Source"), [renderSourceValue(source)]), renderTableCell(_("Close"), [closeButton]) ] ); } function renderStateRow(text, className = "") { return E("tr", { class: "fkp_monitoring-page__state-row" }, [ E( "td", { class: "fkp_monitoring-page__state-cell", colSpan: 8 }, [ E( "div", { class: ["fkp_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: "fkp_monitoring-page__table-wrap" }, [ E( "table", { class: "table cbi-section-table fkp_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 renderConnections(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: _( "Forkop service is stopped. Start the service to display connections." ) }) ); return; } if (loading) { container.replaceChildren( renderConnectionsTable([], { text: _("Loading connections"), className: "fkp_monitoring-page__state--loading" }) ); return; } if (failed) { container.replaceChildren( renderConnectionsTable([], { text: _("Connections are unavailable"), className: "fkp_monitoring-page__state--error" }) ); return; } const visibleConnections = getVisibleConnections(); if (visibleConnections.length === 0) { container.replaceChildren( renderConnectionsTable([], { text: activeTab === "active" ? _("No active connections") : _("No closed connections") }) ); return; } container.replaceChildren(renderConnectionsTable(visibleConnections)); container.scrollLeft = previousScrollLeft; } function flushRenderAfterSelection() { if (!renderSkippedForSelection || isTextSelectionInsideMonitoring()) { return; } renderConnections({ 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; } } renderConnections(); } 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( ".fkp_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); renderConnections(); try { const response = await ForkopShellMethods.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); renderConnections(); } } async function closeAllConnections() { if (activeConnections.size === 0 || closingAll) { return; } closingAll = true; renderControls(); try { const response = await ForkopShellMethods.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(); renderConnections(); } } function bindControls() { const activeButton = document.getElementById("monitoring-tab-active"); const closedButton = document.getElementById("monitoring-tab-closed"); const select = document.getElementById( "monitoring-device-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; renderConnections(); }; } if (searchInput) { searchInput.oninput = () => { searchQuery = searchInput.value; renderConnections(); }; } if (connectionsContainer) { connectionsContainer.onclick = (event) => { const target = event.target; const button = target?.closest( ".fkp_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(); renderConnections(); } } async function loadRouteDisplayNames() { try { buildRouteDisplayNames(await CustomForkopMethods.getConfigSections()); } catch (error) { logger.warn("[MONITORING]", "loadRouteDisplayNames: failed", error); buildRouteDisplayNames([]); } finally { renderControls(); renderConnections(); } } async function pollConnectionsSnapshot() { if (pollingConnections || !monitoringMounted || monitoringPaused || serviceAvailability !== "running") { return; } const mountId = monitoringMountId; pollingConnections = true; try { const response = await ForkopShellMethods.getClashApiConnections(); if (!monitoringMounted || mountId !== monitoringMountId || serviceAvailability !== "running") { return; } if (!response.success) { failed = true; loading = false; renderConnections(); 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; renderConnections(); } 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; renderConnections(); } ); } 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(); renderConnections(); } function watchServiceState() { serviceStateUnsubscribe?.(); serviceStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (!monitoringMounted) { return; } setServiceAvailability( getServiceAvailability({ loading: false, failed: false, running: uiState.service.forkop.running }) ); }); } function resetMonitoringState() { activeTab = "active"; selectedDeviceFilter = ALL_FILTER_VALUE; searchQuery = ""; lastDeviceFilterSignature = ""; 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(); renderConnections(); 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; } renderConnections(); }, 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/forkop/tabs/monitoring/styles.ts var styles5 = ` #cbi-${FORKOP_UCI_PACKAGE}-monitoring-_mount_node { margin: 16px 0 22px; padding: 0; } #cbi-${FORKOP_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-title { display: none; } #cbi-${FORKOP_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-field { margin-left: 0; width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-monitoring-_mount_node > div { width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-monitoring > h3 { display: none; } .fkp_monitoring-page { --fkp-monitoring-control-height: 34px; --fkp-monitoring-row-action-size: 24px; --fkp-monitoring-divider-color: rgba(127, 127, 127, 0.22); --fkp-monitoring-soft-bg: rgba(127, 127, 127, 0.08); --fkp-monitoring-soft-bg-hover: rgba(127, 127, 127, 0.14); --fkp-monitoring-danger-color: var(--error-color-medium, #d32f2f); --fkp-monitoring-success-color: var(--success-color-medium, #2e7d32); --fkp-monitoring-paused-color: var(--primary-color-high, #1976d2); width: 100%; min-width: 0; } .fkp_monitoring-page__panel { margin-top: 0; border: 0; border-radius: 0; padding: 0; background: transparent; box-sizing: border-box; width: 100%; min-width: 0; } .fkp_monitoring-page .btn.fkp_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(--fkp-monitoring-divider-color) !important; border-radius: 6px; background: var(--fkp-monitoring-soft-bg) !important; color: var(--text-color-medium) !important; box-shadow: none; } .fkp_monitoring-page .btn.fkp_monitoring-page__icon-button:hover:not(:disabled) { background: var(--fkp-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .fkp_monitoring-page .btn.fkp_monitoring-page__icon-button--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; } .fkp_monitoring-page .btn.fkp_monitoring-page__icon-button:disabled { opacity: 0.45; cursor: not-allowed; } .fkp_monitoring-page #monitoring-close-all.btn.fkp_monitoring-page__icon-button { order: 2; border-color: rgba(217, 83, 79, 0.4) !important; background: transparent !important; color: #d9534f !important; } .fkp_monitoring-page #monitoring-close-all.btn.fkp_monitoring-page__icon-button:hover:not(:disabled) { border-color: rgba(217, 83, 79, 0.6) !important; background: transparent !important; color: #d9534f !important; } .fkp_monitoring-page #monitoring-pause-toggle.btn.fkp_monitoring-page__icon-button, .fkp_monitoring-page #monitoring-pause-toggle.btn.fkp_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; } .fkp_monitoring-page #monitoring-pause-toggle.btn.fkp_monitoring-page__icon-button:hover:not(:disabled), .fkp_monitoring-page #monitoring-pause-toggle.btn.fkp_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; } .fkp_monitoring-page__icon-button svg, .fkp_monitoring-page__row-action svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } .fkp_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; } .fkp_monitoring-page__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; } .fkp_monitoring-page__tabs { display: inline-flex; align-items: center; gap: 2px; width: max-content; padding: 2px; border: 1px solid var(--fkp-monitoring-divider-color); border-radius: 6px; background: var(--fkp-monitoring-soft-bg); box-sizing: border-box; } .fkp_monitoring-page .btn.fkp_monitoring-page__tab { height: calc(var(--fkp-monitoring-control-height) - 6px); min-height: calc(var(--fkp-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; } .fkp_monitoring-page .btn.fkp_monitoring-page__tab:hover { background: var(--fkp-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .fkp_monitoring-page .btn.fkp_monitoring-page__tab--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; font-weight: 700; } .fkp_monitoring-page__tab-label { display: inline-block; } .fkp_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; } .fkp_monitoring-page__tab--active .fkp_monitoring-page__tab-badge { background: rgba(25, 118, 210, 0.22); color: var(--primary-color-high, #1976d2); } .fkp_monitoring-page__filters { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; } .fkp_monitoring-page__device-filter { width: min(220px, 100%); min-width: 0; height: var(--fkp-monitoring-control-height) !important; min-height: var(--fkp-monitoring-control-height) !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; box-sizing: border-box; line-height: calc(var(--fkp-monitoring-control-height) - 2px) !important; } .fkp_monitoring-page__search { position: relative; display: flex; align-items: center; width: min(320px, 100%); min-width: 0; height: var(--fkp-monitoring-control-height); margin: 0; } .fkp_monitoring-page__search-icon { position: absolute; left: 8px; width: 16px; height: 16px; color: var(--text-color-medium); pointer-events: none; } .fkp_monitoring-page__search-icon svg { width: 16px; height: 16px; display: block; } .fkp_monitoring-page__search-input { width: 100%; height: var(--fkp-monitoring-control-height) !important; min-height: var(--fkp-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(--fkp-monitoring-control-height) - 2px) !important; } .fkp_monitoring-page__body { margin-top: 0; width: 100%; min-width: 0; } .fkp_monitoring-page__table-wrap { width: 100%; overflow-x: auto; margin-bottom: 0; } .fkp_monitoring-page__table { width: 100%; min-width: 840px; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 0; } .fkp_monitoring-page__table th, .fkp_monitoring-page__table td { padding: 8px 6px; border-bottom: 1px solid var(--fkp-monitoring-divider-color); box-sizing: border-box; text-align: left; vertical-align: middle; overflow: hidden; white-space: nowrap; } .fkp_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); } .fkp_monitoring-page__table th:nth-child(1) { width: 28%; } .fkp_monitoring-page__table th:nth-child(2) { width: 6%; } .fkp_monitoring-page__table th:nth-child(3) { width: 16%; } .fkp_monitoring-page__table th:nth-child(4) { width: 8%; } .fkp_monitoring-page__table th:nth-child(5) { width: 9.5%; } .fkp_monitoring-page__table th:nth-child(6) { width: 8.5%; } .fkp_monitoring-page__table th:nth-child(7) { width: 16%; } .fkp_monitoring-page__table th:nth-child(8) { width: 8%; } .fkp_monitoring-page__table tbody tr:last-child td { border-bottom: 0; } .fkp_monitoring-page__table td:last-child { padding-top: 0; padding-bottom: 0; } .fkp_monitoring-page__table th:nth-child(4), .fkp_monitoring-page__table td:nth-child(4), .fkp_monitoring-page__table th:nth-child(5), .fkp_monitoring-page__table td:nth-child(5), .fkp_monitoring-page__table th:nth-child(6), .fkp_monitoring-page__table td:nth-child(6) { text-align: right; } .fkp_monitoring-page__table th:nth-child(7), .fkp_monitoring-page__table td:nth-child(7) { text-align: left; } .fkp_monitoring-page__table th:last-child, .fkp_monitoring-page__table td:last-child { text-align: center; } .fkp_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; } .fkp_monitoring-page__source-value { display: flex; align-items: baseline; justify-content: flex-start; gap: 5px; } .fkp_monitoring-page__source-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fkp_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; } .fkp_monitoring-page__source-value--ip-only { color: var(--text-color-high); } .fkp_monitoring-page__cell-main { color: var(--text-color-high); font-weight: 600; line-height: 1.25; } .fkp_monitoring-page__cell-secondary { margin-top: 2px; color: var(--text-color-medium); font-size: 12px; line-height: 1.25; } .fkp_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; } .fkp_monitoring-page__network { background: transparent; border: 0; padding: 0; color: var(--text-color-medium, #bbb); font-family: inherit; font-size: 13px; text-transform: lowercase; } .fkp_monitoring-page__table td:nth-child(4) .fkp_monitoring-page__value, .fkp_monitoring-page__table td:nth-child(5) .fkp_monitoring-page__value, .fkp_monitoring-page__table td:nth-child(6) .fkp_monitoring-page__value { color: var(--text-color-medium, #bbb); font-family: inherit; text-align: right; } .fkp_monitoring-page .btn.fkp_monitoring-page__row-action { width: var(--fkp-monitoring-row-action-size); height: var(--fkp-monitoring-row-action-size); min-width: var(--fkp-monitoring-row-action-size); min-height: var(--fkp-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(--fkp-monitoring-danger-color) !important; box-shadow: none; cursor: pointer; } .fkp_monitoring-page__row-action svg { width: 14px; height: 14px; } .fkp_monitoring-page .btn.fkp_monitoring-page__row-action:hover:not(:disabled) { background: var(--fkp-monitoring-soft-bg-hover) !important; color: var(--fkp-monitoring-danger-color) !important; } .fkp_monitoring-page .btn.fkp_monitoring-page__row-action:disabled { opacity: 0.45; cursor: wait; } .fkp_monitoring-page__row--closing { opacity: 0.55; } .fkp_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; } .fkp_monitoring-page__state-cell { padding: 0 !important; } .fkp_monitoring-page__state--error { color: var(--error-color-medium, #d32f2f); } @media (max-width: 900px) { .fkp_monitoring-page__controls { align-items: center; } .fkp_monitoring-page__tabs { flex: 1 0 100%; } .fkp_monitoring-page__filters { flex: 1 1 0; } .fkp_monitoring-page__device-filter, .fkp_monitoring-page__search { max-width: none; } .fkp_monitoring-page__table { min-width: 0; } .fkp_monitoring-page__table thead { display: none; } .fkp_monitoring-page__table, .fkp_monitoring-page__table tbody, .fkp_monitoring-page__table tr, .fkp_monitoring-page__table td { display: block; width: 100%; } .fkp_monitoring-page__table tr { border: 1px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 8px; box-sizing: border-box; margin-bottom: 8px; } .fkp_monitoring-page__table td { display: grid; grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); gap: 8px; border: 0; border-bottom: 1px solid var(--fkp-monitoring-divider-color); padding: 4px 0; box-sizing: border-box; text-align: left; } .fkp_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; } .fkp_monitoring-page__table td:last-child { grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); align-items: center; border-bottom: 0; min-height: var(--fkp-monitoring-row-action-size); padding: 0; } .fkp_monitoring-page__value { text-align: right; } .fkp_monitoring-page__source-value { justify-content: flex-end; } .fkp_monitoring-page__state-row td::before { display: none; } } @media (max-width: 520px) { .fkp_monitoring-page__controls, .fkp_monitoring-page__filters { align-items: stretch; } .fkp_monitoring-page__tabs, .fkp_monitoring-page__filters, .fkp_monitoring-page__device-filter, .fkp_monitoring-page__search { width: 100%; } .fkp_monitoring-page__controls, .fkp_monitoring-page__filters { flex-direction: column; } .fkp_monitoring-page__actions { align-self: flex-end; } .fkp_monitoring-page__tabs { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); width: 100%; } .fkp_monitoring-page__table td { grid-template-columns: 1fr; gap: 2px; } .fkp_monitoring-page__value { text-align: left; } .fkp_monitoring-page__source-value { justify-content: flex-start; } } `; // src/forkop/tabs/monitoring/index.ts var MonitoringTab = { render: render3, initController: initController3, styles: styles5 }; // src/forkop/tabs/updates/render.ts function render4() { return E("div", { id: "updates-status", class: "fkp_updates-page" }, [ E("div", { id: "fkp_updates-components", class: "fkp_updates-page__components" }) ]); } // src/forkop/tabs/updates/componentActionCompletion.ts function shouldApplyCompletedComponentActionResult(result, notify) { return result.action !== "check_update" || notify; } // src/forkop/tabs/updates/checkResultLifecycle.ts function shouldPreserveCompletedCheckResultOnNextMount({ action, mounted }) { return action === "check_update" && !mounted; } function shouldResetCheckResultsOnMount({ anyActionLoading, preserveCheckResultsOnNextMount: preserveCheckResultsOnNextMount2, persistentCacheEnabled = false }) { return !persistentCacheEnabled && !anyActionLoading && !preserveCheckResultsOnNextMount2; } function shouldRefreshComponentStateBeforeRender(uiState) { return Boolean( uiState?.actions.component.some((state) => state.running === true) ); } function shouldExposeCheckResults({ mounted, cacheResolved }) { return mounted && cacheResolved; } // src/forkop/tabs/updates/initController.ts var updatesLifecycleRegistered = false; var updatesControllerInitialized = false; var updatesMounted = false; var updatesMountId = 0; var pageUnloading2 = false; var preserveCheckResultsOnNextMount = false; var componentUpdateCheckCacheResolved = false; var componentUpdateCheckCacheSnapshot = null; var componentUpdateCheckCachePromise = null; var componentActionStateUnsubscribe = null; var componentActionStateRefreshPromise = null; var followedComponentJobs = /* @__PURE__ */ new Set(); var handledComponentJobs = /* @__PURE__ */ new Set(); if (typeof window !== "undefined") { window.addEventListener("pagehide", () => { pageUnloading2 = true; }); window.addEventListener("pageshow", () => { pageUnloading2 = false; }); } function isNotInstalled(version) { return !version || version === "not installed"; } function shouldShowInstallAfterCheck(component) { const status = getVisibleCheckResult(component)?.status; return status === "outdated" || status === "dev"; } 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 isServiceRuntimeActionLoading() { const state = store.get(); return hasLocalMutatingServiceActionLoading(state.diagnosticsActions) || isServiceTransitionStatus(state.servicesInfoWidget.data.forkopStatus); } 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 = "") { const updatesChecks = store.get().updatesChecks; store.set({ updatesChecks: { ...updatesChecks, [component]: { status, latest_version: latestVersion, release_url: releaseUrl } } }); } function resetCheckResult(component) { setCheckResult(component, null, ""); } function applyCachedCheckResults(results) { results.forEach((result) => { const status = result.status || null; if (status === "latest" || status === "outdated" || status === "dev") { setCheckResult( result.component, status, result.latest_version || "", result.release_url || "" ); } }); } function loadComponentUpdateCheckCache({ force = false } = {}) { if (!force && componentUpdateCheckCacheSnapshot) { return Promise.resolve(componentUpdateCheckCacheSnapshot); } if (componentUpdateCheckCachePromise) { return componentUpdateCheckCachePromise; } const promise = ForkopShellMethods.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 ForkopShellMethods.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 !== "forkop" || button.action !== "install") { return void 0; } return store.get().updatesChecks[button.component].latest_version || void 0; } function getCheckToastMessage(status) { if (status === "outdated") { 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(FORKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT, { detail: { zapretInstalled: Boolean(systemInfo.zapret_installed), zapret2Installed: Boolean(systemInfo.zapret2_installed), byedpiInstalled: Boolean(systemInfo.byedpi_installed) } }) ); } function reloadPageAfterForkopUpdate() { window.setTimeout(() => { window.location.reload(); }, 1200); } function patchSystemInfoAfterMutation(result) { const systemInfo = store.get().diagnosticsSystemInfo; const nextSystemInfo = { ...systemInfo, loading: false, loaded: true }; const version = result.current_version || result.latest_version || _("unknown"); if (result.component === "forkop" && result.action === "install") { nextSystemInfo.forkop_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_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_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_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_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 }) { if (result.action === "check_update") { setActionLoading(key, false); if (!shouldApplyCompletedComponentActionResult(result, notify)) { return; } if (shouldPreserveCompletedCheckResultOnNextMount({ action: result.action, mounted: updatesMounted })) { preserveCheckResultsOnNextMount = true; } const status = result.status || null; if (status === "latest" || status === "outdated" || status === "dev") { setCheckResult( result.component, status, result.latest_version || "", result.release_url || "" ); } if (notify) { showToast(getCheckToastMessage(status), "success"); } return; } if (result.action === "install" || result.action.startsWith("install_")) { setCheckResult(result.component, "latest", result.latest_version || ""); } else { resetCheckResult(result.component); } patchSystemInfoAfterMutation(result); setActionLoading(key, false); if (result.component === "forkop" && result.action === "install") { if (notify && result.message) { showToast(result.message, "success", 1200); } if (notify) { reloadPageAfterForkopUpdate(); } return; } if (notify && result.message) { showToast(result.message, "success"); } 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); if (!response.success || response.data.success === false) { const message = response.success ? response.data.message || _("Failed to execute") : response.error || _("Failed to execute"); if (isTransientRpcError(message)) { setActionLoading(key, false); void refreshComponentActionState(); return; } handledComponentJobs.add(jobId); setActionLoading(key, false); if (shouldNotify) { showToast(message, "error"); } await ackComponentActionJob(jobId); return; } handledComponentJobs.add(jobId); await ackComponentActionJob(jobId); await applyCompletedComponentAction({ key, result: response.data, notify: shouldNotify }); } async function followComponentActionState(state) { const jobId = state.job_id; const key = getComponentActionKey(state.component, state.action); if (!jobId || !key || followedComponentJobs.has(jobId)) { return; } if (!state.running && handledComponentJobs.has(jobId)) { return; } followedComponentJobs.add(jobId); if (shouldShowLoadingForRestoredAction(state)) { setActionLoading(key, true); } try { const response = state.running ? await ForkopShellMethods.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"); } } } finally { followedComponentJobs.delete(jobId); } } async function followAlreadyRunningComponentAction(button) { const uiState = await refreshRuntimeUiState({ force: true }); if (!uiState) { return false; } const state = uiState.actions.component.find( (item) => item.running && item.component === button.component && item.action === button.action ); if (!state) { return false; } if (state.job_id) { markUiActionOwned("component", state.job_id); } await followComponentActionState(state); return true; } function isComponentActionAlreadyRunningError(message) { return Boolean( message && message.includes("Another component action is already running") ); } function handleComponentUiState(uiState) { for (const state of uiState.actions.component || []) { void followComponentActionState(state); } } async function refreshComponentActionState() { if (componentActionStateRefreshPromise) { return componentActionStateRefreshPromise; } componentActionStateRefreshPromise = (async () => { if (!updatesMounted) { return; } const state = await refreshRuntimeUiState({ force: true }); if (!state) { return; } handleComponentUiState(state); })().finally(() => { componentActionStateRefreshPromise = null; }); return componentActionStateRefreshPromise; } function startComponentActionStateWatcher() { if (componentActionStateUnsubscribe) { return; } componentActionStateUnsubscribe = subscribeRuntimeUiState((uiState) => { if (updatesMounted) { handleComponentUiState(uiState); } }); } function stopComponentActionStateWatcher() { if (!componentActionStateUnsubscribe) { return; } componentActionStateUnsubscribe(); componentActionStateUnsubscribe = null; } async function handleComponentAction(button) { if (!beginComponentAction(button)) { return; } let jobId = ""; let ownsJobFollow = false; try { const startResponse = await ForkopShellMethods.componentActionStart( button.component, button.action ); if (!startResponse.success) { if (isComponentActionAlreadyRunningError(startResponse.error)) { setActionLoading(button.key, false); if (!await followAlreadyRunningComponentAction(button)) { await refreshComponentActionState(); } return; } if (isTransientRpcError(startResponse.error)) { if (!await followAlreadyRunningComponentAction(button)) { setActionLoading(button.key, false); await refreshComponentActionState(); } return; } throw new Error(startResponse.error); } jobId = startResponse.data.job_id; markUiActionOwned("component", jobId); if (followedComponentJobs.has(jobId)) { return; } followedComponentJobs.add(jobId); ownsJobFollow = true; const response = await ForkopShellMethods.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"); } 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 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" } ]; } 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; const singBoxExtendedCompressed = Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_compressed); const singBoxTiny = Boolean(systemInfo.sing_box_tiny); const forkopActions = getInstalledUpdateActions( "forkop", "forkopCheck", "forkopInstall" ); 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" }); } 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: "forkop", column: 0, title: "Forkop", version: systemInfoLoading ? _("Loading...") : normalizeCompiledVersion(systemInfo.forkop_version), latestVersion: getLatestVersion("forkop"), releaseUrl: getGitHubReleaseUrl("forkop"), actions: forkopActions }, { component: "sing_box", column: 0, title: "Sing-box", version: systemInfoLoading ? _("Loading...") : formatSingBoxVersion(systemInfo), latestVersion: getLatestVersion("sing_box"), releaseUrl: getGitHubReleaseUrl("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"), actions: zapretActions }, { component: "zapret2", column: 1, title: "Zapret2", version: systemInfoLoading ? _("Loading...") : zapret2Installed ? systemInfo.zapret2_version : _("Not installed"), latestVersion: getLatestVersion("zapret2"), releaseUrl: getGitHubReleaseUrl("zapret2"), actions: zapret2Actions }, { component: "byedpi", column: 1, title: "ByeDPI", version: systemInfoLoading ? _("Loading...") : byedpiInstalled ? systemInfo.byedpi_version : _("Not installed"), latestVersion: getLatestVersion("byedpi"), releaseUrl: getGitHubReleaseUrl("byedpi"), actions: byedpiActions } ]; } function renderComponentCard(card) { const updatesActions = store.get().updatesActions; const anyActionLoading = isAnyActionLoading(); const serviceRuntimeActionLoading = isServiceRuntimeActionLoading(); const systemInfoLoading = isSystemInfoLoading(); const headerChildren = [ E("b", { class: "fkp_updates-page__component__title" }, card.title), E( "span", { class: "fkp_updates-page__component__header-version" }, card.version ) ]; const header = E( "div", { class: "fkp_updates-page__component__header" }, headerChildren ); const detailsChildren = []; const checkResult = getVisibleCheckResult(card.component); 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: "fkp_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 === "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: "fkp_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: "fkp_updates-page__component__info-label" }, labelText ) ]; if (latestValueNodes.length > 0) { rowChildren.push( E( "span", { class: "fkp_updates-page__component__info-value fkp_updates-page__component__info-value--latest" }, latestValueNodes ) ); } detailsChildren.push( E( "div", { class: "fkp_updates-page__component__info-row" }, rowChildren ) ); } } const detailsContainer = detailsChildren.length > 0 ? E( "div", { class: "fkp_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"; return renderButton({ classNames: isUpdateOrInstall ? ["cbi-button-save"] : [], text: action.text, icon: action.icon, loading: loading2, disabled: systemInfoLoading || serviceRuntimeActionLoading || 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 || serviceRuntimeActionLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }); if (primaryButtons.length > 0 || dangerButtons.length > 0) { actionElements.push( E("div", { class: "fkp_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 || serviceRuntimeActionLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }); actionElements.push( E("div", { class: "fkp_updates-page__component__variants" }, [ E( "div", { class: "fkp_updates-page__component__variants-title" }, _("Install another build:") ), E( "div", { class: "fkp_updates-page__component__variants-buttons" }, variantButtons ) ]) ); } const actionsContainer = E( "div", { class: [ "fkp_updates-page__component__actions", detailsContainer ? "fkp_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: "fkp_updates-page__component" }, cardChildren); } function renderUpdatesComponents() { const container = document.getElementById("fkp_updates-components"); if (!container) { return; } const columns = [[], []]; getComponentCards().forEach((card) => { columns[card.column].push(renderComponentCard(card)); }); return preserveScrollForPage(() => { container.replaceChildren( E("div", { class: "fkp_updates-page__components-column" }, columns[0]), E("div", { class: "fkp_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/forkop/tabs/updates/styles.ts var styles6 = ` #cbi-${FORKOP_UCI_PACKAGE}-updates-_mount_node > div { width: 100%; } #cbi-${FORKOP_UCI_PACKAGE}-updates > h3 { display: none; } .fkp_updates-page { width: 100%; } .fkp_updates-page__components { display: flex; align-items: flex-start; gap: 10px; width: 100%; flex-wrap: wrap; } .fkp_updates-page__components-column { display: flex; flex: 1 1 auto; flex-direction: column; gap: 10px; min-width: max-content; } @media (max-width: 760px) { .fkp_updates-page__components { flex-direction: column; } .fkp_updates-page__components-column { width: 100%; min-width: 0; } } .fkp_updates-page__component { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; gap: 10px; background-color: var(--background-color-high, #fff); min-width: max-content; } .fkp_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; } .fkp_updates-page__component__title { color: var(--text-color-high); font-size: 16px; font-weight: bold; line-height: 1.2; } .fkp_updates-page__component__header-version { color: var(--text-color-medium, #888); font-size: 13px; font-weight: normal; } .fkp_updates-page__component__details { display: flex; flex-direction: column; gap: 6px; } .fkp_updates-page__component__info-row { display: flex; justify-content: flex-start; align-items: center; min-height: 24px; gap: 8px; white-space: nowrap; } .fkp_updates-page__component__info-label { color: var(--text-color-medium, #888); font-size: 12px; } .fkp_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; } .fkp_updates-page__component__info-value--latest { flex-wrap: wrap; justify-content: flex-start; } .fkp_updates-page__component__release-version-link { color: var(--link-color, #3498db) !important; text-decoration: underline; font-weight: bold; } .fkp_updates-page__component__release-version-link:hover { color: var(--link-color-dark, #2980b9) !important; } .fkp_updates-page__component__actions { display: flex; flex-direction: column; gap: 10px; margin-top: auto; } .fkp_updates-page__component__actions--with-details { border-top: 1px var(--background-color-low, lightgray) solid; padding-top: 10px; } .fkp_updates-page__component__actions-main { display: flex; justify-content: flex-start; align-items: center; flex-wrap: nowrap; gap: 6px; } .fkp_updates-page__component__variants { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; } .fkp_updates-page__component__variants-title { font-size: 11px; font-weight: bold; color: var(--text-color-medium, gray); } .fkp_updates-page__component__variants-buttons { display: flex; flex-wrap: nowrap; gap: 6px; } `; // src/forkop/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-${FORKOP_UCI_PACKAGE}-settings > h3 { display: none; } /* Hide extra H3 for rules tab */ #cbi-${FORKOP_UCI_PACKAGE}-section > h3:nth-child(1) { display: none; } /* Vertical align for remove rule action button */ #cbi-${FORKOP_UCI_PACKAGE}-section > .cbi-section-remove { margin-bottom: -32px; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-actions > div { display: inline-flex; align-items: center; gap: 4px; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-actions { text-align: right; } /* Rule reorder visuals */ #cbi-${FORKOP_UCI_PACKAGE}-section { position: relative; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-table-row { position: relative; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-table-row.placeholder { opacity: 1; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-table-row.placeholder em { font-style: italic; } #cbi-${FORKOP_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after, #cbi-${FORKOP_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-${FORKOP_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after { top: -1px; } #cbi-${FORKOP_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 FORKOP_GLOBAL_STYLES_ID = "forkop-global-styles"; function injectGlobalStyles() { if (document.getElementById(FORKOP_GLOBAL_STYLES_ID)) { return; } document.head.insertAdjacentHTML( "beforeend", ` ` ); } // src/main.ts if (typeof structuredClone !== "function") globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj)); return baseclass.extend({ ALLOWED_WITH_RUSSIA_INSIDE, BOOTSTRAP_DNS_SERVER_OPTIONS, DEFAULT_LATENCY_TEST_URL, DNS_SERVER_OPTIONS, DOMAIN_LIST_OPTIONS, DashboardTab, DiagnosticTab, FORKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT, FORKOP_UCI_PACKAGE, ForkopShellMethods, LATENCY_TEST_URL_OPTIONS, MonitoringTab, REGIONAL_OPTIONS, UpdatesTab, applyUiStateToStore, bulkValidate, coreService, getClashUIUrl, getProxyUrlName, injectGlobalStyles, parseValueList, showToast, store, validateDNS, validateDomain, validateIP, validateIPV4, validateIPv6, validateOutboundJson, validatePath, validateProxyUrl, validateSubnet, validateUrl });