// This file is autogenerated, please don't change manually "use strict"; "require baseclass"; "require fs"; "require uci"; "require ui"; // src/validators/validateIp.ts function validateIPV4(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])$/; if (ipRegex.test(ip)) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _("Invalid IP address") }; } // src/validators/validateDomain.ts function validateDomain(domain, allowDotTLD = false) { const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; if (allowDotTLD) { const dotTLD = /^\.[a-zA-Z]{2,}$/; if (dotTLD.test(domain)) { return { valid: true, message: _("Valid") }; } } if (!domainRegex.test(domain)) { return { valid: false, message: _("Invalid domain address") }; } const hostname = domain.split("/")[0]; const parts = hostname.split("."); const atLeastOneInvalidPart = parts.some((part) => part.length > 63); if (atLeastOneInvalidPart) { return { valid: false, message: _("Invalid domain address") }; } return { valid: true, message: _("Valid") }; } // src/validators/validateDns.ts function validateDNS(value) { if (!value) { return { valid: false, message: _("DNS server address cannot be empty") }; } const cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, ""); const cleanedIpWithoutPath = cleanedValueWithoutPort.split("/")[0]; if (validateIPV4(cleanedIpWithoutPath).valid) { return { valid: true, message: _("Valid") }; } if (validateDomain(cleanedValueWithoutPort).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(", ") }; const regex = new RegExp( `^(?:${protocols.map((p) => p.replace(":", "")).join("|")})://(?:(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}|(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}|localhost)(?::\\d+)?(?:/[^\\s]*)?$` ); if (regex.test(url)) { return { valid: true, message: _("Valid") }; } 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 subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/; if (!subnetRegex.test(value)) { return { valid: false, message: _("Invalid format. Use X.X.X.X or X.X.X.X/Y") }; } const [ip, cidr] = value.split("/"); if (ip === "0.0.0.0" && cidr == null) { return { valid: false, message: _("IP address 0.0.0.0 is not allowed") }; } const ipCheck = validateIPV4(ip); if (!ipCheck.valid) { return ipCheck; } if (cidr) { const cidrNum = parseInt(cidr, 10); if (cidrNum < 0 || cidrNum > 32) { return { valid: false, message: _("CIDR must be between 0 and 32") }; } } 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 function validateOutboundJson(value) { try { JSON.parse(value); return { valid: true, message: _("Valid") }; } catch { return { valid: false, message: _("Invalid JSON format") }; } } // 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 [server, portAndRest] = serverPart.split(":"); 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 [host, port] = hostPortPart.split(":"); 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/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 [host, port] = hostPortPart.split(":"); 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 [host, port] = hostPortPart.split(":"); 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") }; } const ipv4Result = validateIPV4(host); const domainResult = validateDomain(host); if (!ipv4Result.valid && !domainResult.valid) { 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 [host, port] = hostPortPart.split(":"); 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") }; } } return { valid: true, message: _("Valid") }; } catch (_e) { return { valid: false, message: _("Invalid HY2 URL: parsing failed") }; } } // src/validators/validateProxyUrl.ts function validateProxyUrl(url) { const trimmedUrl = url.trim(); if (trimmedUrl.startsWith("ss://")) { return validateShadowsocksUrl(trimmedUrl); } if (trimmedUrl.startsWith("vless://")) { return validateVlessUrl(trimmedUrl); } if (trimmedUrl.startsWith("trojan://")) { return validateTrojanUrl(trimmedUrl); } if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { return validateHysteria2Url(trimmedUrl); } return { valid: false, message: _( "URL must start with vless://, ss://, trojan://, socks4://, socks4a://, socks5://, 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/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/podkop/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 PODKOP_UCI_PACKAGE = "podkop-plus"; var PODKOP_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__"; var PODKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT = "podkop:action-providers-availability"; var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi"; var IP_CHECK_DOMAIN = "ip.podkop.fyi"; 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", 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/getProxyUrlName.ts function getProxyUrlName(url) { try { const [_link, hash] = url.split("#"); if (!hash) { return ""; } return decodeURIComponent(hash); } catch { return ""; } } // 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 getClashWsUrl() { const { hostname } = window.location; return `ws://${hostname}:9090`; } function getClashHttpUrl() { const { hostname } = window.location; return `http://${hostname}:9090`; } function getClashUIUrl() { const { hostname } = window.location; return `http://${hostname}:9090/ui`; } // src/helpers/preserveScrollForPage.ts function preserveScrollForPage(renderFn) { const scrollY = window.scrollY; renderFn(); requestAnimationFrame(() => { window.scrollTo({ top: scrollY }); }); } // src/helpers/svgEl.ts function svgEl(tag, attrs = {}, children = []) { const NS = "http://www.w3.org/2000/svg"; const el = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) { if (v != null) el.setAttribute(k, String(v)); } (Array.isArray(children) ? children : [children]).filter(Boolean).forEach((ch) => el.appendChild(ch)); return el; } // src/helpers/insertIf.ts function insertIf(condition, elements) { return condition ? elements : []; } // src/helpers/isCopyableProxyLink.ts var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i; function isCopyableProxyLink(link) { return COPYABLE_PROXY_URI_RE.test((link || "").trim()); } // src/icons/renderLoaderCircleIcon24.ts function renderLoaderCircleIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-loader-circle rotate" }, [ svgEl("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }), svgEl("animateTransform", { attributeName: "transform", attributeType: "XML", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "1s", repeatCount: "indefinite" }) ] ); } // src/icons/renderCircleAlertIcon24.ts function renderCircleAlertIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-alert-icon lucide-circle-alert" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("line", { x1: "12", y1: "8", x2: "12", y2: "12" }), svgEl("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }) ] ); } // src/icons/renderCircleCheckIcon24.ts function renderCircleCheckIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-check-icon lucide-circle-check" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M9 12l2 2 4-4" }) ] ); } // src/icons/renderCircleSlashIcon24.ts function renderCircleSlashIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-slash-icon lucide-circle-slash" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("line", { x1: "9", y1: "15", x2: "15", y2: "9" }) ] ); } // src/icons/renderCircleXIcon24.ts function renderCircleXIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-x-icon lucide-circle-x" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M15 9L9 15" }), svgEl("path", { d: "M9 9L15 15" }) ] ); } // src/icons/renderCheckIcon24.ts function renderCheckIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-check-icon lucide-check" }, [ svgEl("path", { d: "M20 6 9 17l-5-5" }) ] ); } // src/icons/renderXIcon24.ts function renderXIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-x-icon lucide-x" }, [svgEl("path", { d: "M18 6 6 18" }), svgEl("path", { d: "m6 6 12 12" })] ); } // src/icons/renderTriangleAlertIcon24.ts function renderTriangleAlertIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-triangle-alert-icon lucide-triangle-alert" }, [ svgEl("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }), svgEl("path", { d: "M12 9v4" }), svgEl("path", { d: "M12 17h.01" }) ] ); } // src/icons/renderPauseIcon24.ts function renderPauseIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-pause-icon lucide-pause" }, [ svgEl("rect", { x: "14", y: "3", width: "5", height: "18", rx: "1" }), svgEl("rect", { x: "5", y: "3", width: "5", height: "18", rx: "1" }) ] ); } // src/icons/renderPlayIcon24.ts function renderPlayIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-play-icon lucide-play" }, [ svgEl("path", { d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" }) ] ); } // src/icons/renderRotateCcwIcon24.ts function renderRotateCcwIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" }, [ svgEl("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), svgEl("path", { d: "M3 3v5h5" }) ] ); } // src/icons/renderCircleStopIcon24.ts function renderCircleStopIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-stop-icon lucide-circle-stop" }, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("rect", { x: "9", y: "9", width: "6", height: "6", rx: "1" }) ] ); } // src/icons/renderCirclePlayIcon24.ts function renderCirclePlayIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-play-icon lucide-circle-play" }, [ svgEl("path", { d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" }), svgEl("circle", { cx: "12", cy: "12", r: "10" }) ] ); } // src/icons/renderCircleCheckBigIcon24.ts function renderCircleCheckBigIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" }, [ svgEl("path", { d: "M21.801 10A10 10 0 1 1 17 3.335" }), svgEl("path", { d: "m9 11 3 3L22 4" }) ] ); } // src/icons/renderSquareChartGanttIcon24.ts function renderSquareChartGanttIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" }, [ svgEl("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }), svgEl("path", { d: "M9 8h7" }), svgEl("path", { d: "M8 12h6" }), svgEl("path", { d: "M11 16h5" }) ] ); } // src/icons/renderCogIcon24.ts function renderCogIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-cog-icon lucide-cog" }, [ svgEl("path", { d: "M11 10.27 7 3.34" }), svgEl("path", { d: "m11 13.73-4 6.93" }), svgEl("path", { d: "M12 22v-2" }), svgEl("path", { d: "M12 2v2" }), svgEl("path", { d: "M14 12h8" }), svgEl("path", { d: "m17 20.66-1-1.73" }), svgEl("path", { d: "m17 3.34-1 1.73" }), svgEl("path", { d: "M2 12h2" }), svgEl("path", { d: "m20.66 17-1.73-1" }), svgEl("path", { d: "m20.66 7-1.73 1" }), svgEl("path", { d: "m3.34 17 1.73-1" }), svgEl("path", { d: "m3.34 7 1.73 1" }), svgEl("circle", { cx: "12", cy: "12", r: "2" }), svgEl("circle", { cx: "12", cy: "12", r: "8" }) ] ); } // src/icons/renderSearchIcon24.ts function renderSearchIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-search-icon lucide-search" }, [ svgEl("path", { d: "m21 21-4.34-4.34" }), svgEl("circle", { cx: "11", cy: "11", r: "8" }) ] ); } // src/icons/renderBookOpenTextIcon24.ts function renderBookOpenTextIcon24() { const NS = "http://www.w3.org/2000/svg"; return svgEl( "svg", { xmlns: NS, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-book-open-text-icon lucide-book-open-text" }, [ svgEl("path", { d: "M12 7v14" }), svgEl("path", { d: "M16 12h2" }), svgEl("path", { d: "M16 8h2" }), svgEl("path", { d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" }), svgEl("path", { d: "M6 12h2" }), svgEl("path", { d: "M6 8h2" }) ] ); } // src/icons/renderCopyIcon24.ts function renderCopyIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-copy-icon lucide-copy" }, [ svgEl("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }), svgEl("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }) ] ); } // src/icons/renderLinkIcon24.ts function renderLinkIcon24() { return svgEl( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-link-icon lucide-link" }, [ svgEl("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), svgEl("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }) ] ); } // src/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/podkop/tabs/dashboard/partials/renderSections.ts var REGION_NAME_FALLBACKS = { XK: "Kosovo" }; var regionDisplayNamesCache = {}; function getLuciLanguage() { const luci = globalThis.L; if (luci?.env?.lang) { return `${luci.env.lang}`.replace("_", "-"); } if (document.documentElement.lang) { return document.documentElement.lang; } return navigator.language || "en"; } function getCountryDisplayName(country) { const code = `${country || ""}`.trim().toUpperCase(); if (!/^[A-Z]{2}$/.test(code)) { return ""; } const language = getLuciLanguage(); const cacheKey = `${language}:${code}`; if (regionDisplayNamesCache[cacheKey]) { return regionDisplayNamesCache[cacheKey]; } try { const displayNamesConstructor = Intl.DisplayNames; if (displayNamesConstructor) { const displayNames = new displayNamesConstructor([language, "en"], { type: "region" }); const displayName = displayNames.of(code); if (displayName && displayName !== code) { regionDisplayNamesCache[cacheKey] = displayName; return displayName; } } } catch (_error) { } const fallback = REGION_NAME_FALLBACKS[code] || code; regionDisplayNamesCache[cacheKey] = fallback; return fallback; } function getCountryFlagEmoji(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 renderCountryFlag(country) { const countryFlag = getCountryFlagEmoji(country); if (!countryFlag) { return void 0; } const countryName = getCountryDisplayName(country); return E( "span", { class: "pdk_dashboard-page__outbound-grid__item__country", title: countryName || void 0, "aria-label": countryName || void 0 }, countryFlag ); } function renderFailedState() { return E( "div", { class: "pdk_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: "pdk_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 pdk_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: "pdk_dashboard-page__subscription-meta" }, [ E("div", { class: "pdk_dashboard-page__subscription-meta__main" }, [ E( "div", { class: "pdk_dashboard-page__subscription-meta__heading" }, _("Subscription info:") ), title ? E( "div", { class: "pdk_dashboard-page__subscription-meta__title" }, title ) : "", rows.length ? E( "div", { class: "pdk_dashboard-page__subscription-meta__facts" }, rows.map( (row) => E( "div", { class: "pdk_dashboard-page__subscription-meta__fact" }, [ E( "span", { class: "pdk_dashboard-page__subscription-meta__fact-key" }, row.label ), E( "span", { class: "pdk_dashboard-page__subscription-meta__fact-value" }, row.value ) ] ) ) ) : "", actions.length ? E( "div", { class: "pdk_dashboard-page__subscription-meta__actions" }, actions ) : "" ]), metadata.announce ? E( "blockquote", { class: "pdk_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 pdk_dashboard-page__outbound-section__subscription-update", title: _("Update subscriptions"), "aria-label": _("Update subscriptions"), disabled: subscriptionUpdating ? true : void 0, click: (event) => { event.stopPropagation(); if (subscriptionUpdating) { return; } onUpdateSubscription(section); } }, subscriptionUpdating ? renderLoaderCircleIcon24() : renderRotateCcwIcon24() ); } function renderDefaultState({ section, onChooseOutbound, onCopyOutbound, onTestLatency, onUpdateSubscription, latencyFetching, subscriptionUpdating, selectorSwitchingTag }) { function testLatency() { if (section.withTagSelect) { return onTestLatency(section.latencyTestCode || section.code); } if (section.outbounds.length) { return onTestLatency(section.outbounds[0].code); } } function renderOutbound(outbound) { function getLatencyClass() { if (!outbound.latency) { return "pdk_dashboard-page__outbound-grid__item__latency--empty"; } if (outbound.latency < 800) { return "pdk_dashboard-page__outbound-grid__item__latency--green"; } if (outbound.latency < 1500) { return "pdk_dashboard-page__outbound-grid__item__latency--yellow"; } return "pdk_dashboard-page__outbound-grid__item__latency--red"; } const canCopyLink = Boolean(outbound.canCopyLink) || isCopyableProxyLink(outbound.link); const countryFlag = renderCountryFlag(outbound.country); const selectorSwitching = Boolean(selectorSwitchingTag); const outboundSwitching = selectorSwitchingTag === outbound.code; const canChooseOutbound = section.withTagSelect && !selectorSwitching && !outbound.selected; const className = [ "pdk_dashboard-page__outbound-grid__item", outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : "", canChooseOutbound ? "pdk_dashboard-page__outbound-grid__item--selectable" : "", section.withTagSelect && !canChooseOutbound ? "pdk_dashboard-page__outbound-grid__item--disabled" : "", outboundSwitching ? "pdk_dashboard-page__outbound-grid__item--switching" : "" ].filter(Boolean).join(" "); const typeChildren = countryFlag ? [countryFlag, outbound.type ? ` ${outbound.type}` : ""] : [outbound.type].filter(Boolean); 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: "pdk_dashboard-page__outbound-grid__item__snake" }, [ svgEl("rect", { width: "100%", height: "100%", fill: "none", rx: 4, ry: 4, pathLength: 100 }) ] ) ] : [], E("div", { class: "pdk_dashboard-page__outbound-grid__item__header" }, [ E("b", {}, outbound.displayName), ...canCopyLink ? [ E( "button", { type: "button", class: "btn pdk_dashboard-page__outbound-grid__item__copy-button", title: _("Copy proxy link"), "aria-label": _("Copy proxy link"), click: (event) => { event.stopPropagation(); onCopyOutbound(section, outbound); } }, renderCopyIcon24() ) ] : [] ]), E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [ E( "div", { class: "pdk_dashboard-page__outbound-grid__item__type" }, typeChildren ), 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: "pdk_dashboard-page__outbound-section" }, [ // Title with test latency E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [ E( "div", { class: "pdk_dashboard-page__outbound-section__title-section__title" }, section.displayName ), E( "div", { class: "pdk_dashboard-page__outbound-section__title-section__actions" }, [ ...subscriptionUpdateAction ? [subscriptionUpdateAction] : [], E( "button", { class: "btn dashboard-sections-grid-item-test-latency", disabled: latencyFetching ? true : void 0, click: () => { if (latencyFetching) { return; } testLatency(); } }, latencyFetching ? [renderLoaderCircleIcon24(), _("Test latency")] : _("Test latency") ) ] ) ]), E("div", { class: "pdk_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/podkop/tabs/dashboard/partials/renderWidget.ts function renderFailedState2() { return E( "div", { id: "", style: "height: 78px", class: "pdk_dashboard-page__widgets-section__item centered" }, _("Currently unavailable") ); } function renderLoadingState2() { return E( "div", { id: "", style: "height: 78px", class: "pdk_dashboard-page__widgets-section__item skeleton" }, "" ); } function renderDefaultState2({ title, items }) { return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [ E( "b", { class: "pdk_dashboard-page__widgets-section__item__title" }, title ), ...items.map( (item) => E( "div", { class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` }, [ E( "span", { class: "pdk_dashboard-page__widgets-section__item__row__key" }, `${item.key}: ` ), E( "span", { class: "pdk_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/podkop/tabs/dashboard/render.ts function render() { return E( "div", { id: "dashboard-status", class: "pdk_dashboard-page" }, [ // Widgets section E("div", { class: "pdk_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: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, 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(_("Successfully copied!"), "success"); } catch (_err) { showToast(_("Failed to copy!"), "error"); console.error("copyToClipboard - e", _err); } document.body.removeChild(textarea); } // src/podkop/methods/custom/getConfigSections.ts async function getConfigSections() { return uci.load(PODKOP_UCI_PACKAGE).then(() => uci.sections(PODKOP_UCI_PACKAGE)); } // src/podkop/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", "dns-in", "service-mixed-in", "direct-out" ]); function hasReservedNumberedParent(base, postfix) { const match = base.match(/^(.*)-\d+$/); return Boolean(match && RESERVED_RUNTIME_TAGS.has(`${match[1]}-${postfix}`)); } function allocateRuntimeTag(base, postfix) { let suffix = hasReservedNumberedParent(base, postfix) ? 1 : 0; let candidate = suffix > 0 ? `${base}-${suffix}-${postfix}` : `${base}-${postfix}`; while (RESERVED_RUNTIME_TAGS.has(candidate)) { suffix += 1; candidate = `${base}-${suffix}-${postfix}`; } return candidate; } function getOutboundTagBySection(sectionName) { return allocateRuntimeTag(sectionName, "out"); } // src/podkop/methods/shell/callBaseMethod.ts async function callBaseMethod(method, args = [], command = "/usr/bin/podkop-plus", 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/podkop/types.ts var Podkop; ((Podkop2) => { 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_BYEDPI_RUNTIME"] = "check_byedpi_runtime"; 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["GET_SING_BOX_STATUS"] = "get_sing_box_status"; AvailableMethods2["GET_ZAPRET_STATUS"] = "get_zapret_status"; AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status"; AvailableMethods2["CLASH_API"] = "clash_api"; AvailableMethods2["RESTART"] = "restart"; AvailableMethods2["START"] = "start"; AvailableMethods2["STOP"] = "stop"; 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["COMPONENT_ACTION"] = "component_action"; AvailableMethods2["COMPONENT_ACTION_ASYNC"] = "component_action_async"; AvailableMethods2["COMPONENT_ACTION_STATUS"] = "component_action_status"; AvailableMethods2["SUBSCRIPTION_UPDATE"] = "subscription_update"; })(AvailableMethods = Podkop2.AvailableMethods || (Podkop2.AvailableMethods = {})); let AvailableClashAPIMethods; ((AvailableClashAPIMethods2) => { AvailableClashAPIMethods2["GET_PROXIES"] = "get_proxies"; AvailableClashAPIMethods2["GET_CONNECTIONS"] = "get_connections"; AvailableClashAPIMethods2["GET_PROXY_LATENCY"] = "get_proxy_latency"; 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 = Podkop2.AvailableClashAPIMethods || (Podkop2.AvailableClashAPIMethods = {})); })(Podkop || (Podkop = {})); // src/podkop/methods/shell/index.ts var SUBSCRIPTION_UPDATE_TIMEOUT_MS = 10 * 60 * 1e3; var COMPONENT_ACTION_TIMEOUT_MS = 10 * 60 * 1e3; var COMPONENT_ACTION_RPC_TIMEOUT_MS = 15e3; var COMPONENT_ACTION_POLL_INTERVAL_MS = 1500; var COMPONENT_ACTION_SELF_UPDATE_SETTLE_MS = 3e4; var COMPONENT_ACTION_STATE_DIR = "/var/run/podkop-plus/component-actions"; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function parseComponentActionOutput(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 parseComponentActionResult(response) { return parseComponentActionOutput(response.stdout); } function parseComponentActionStartResult(response) { const parsedResponse = parseComponentActionResult(response); if (!parsedResponse) { return null; } return parsedResponse; } 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 readPodkopVersion() { const response = await executeShellCommand({ command: "/usr/bin/podkop-plus", args: ["show_version"], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); if ((response.code ?? 0) !== 0 || !response.stdout) { return ""; } return response.stdout.trim(); } function componentActionFailure(response, parsedResponse) { return { success: false, error: parsedResponse?.message || response.stderr || _("Failed to execute") }; } var PodkopShellMethods = { checkDNSAvailable: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_DNS_AVAILABLE ), checkFakeIP: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_FAKEIP ), checkNftRules: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_NFT_RULES ), checkZapretRuntime: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_ZAPRET_RUNTIME ), checkByedpiRuntime: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_BYEDPI_RUNTIME ), getStatus: async () => callBaseMethod(Podkop.AvailableMethods.GET_STATUS), getOutboundLink: async (section, tag) => callBaseMethod( Podkop.AvailableMethods.GET_OUTBOUND_LINK, [section, tag] ), getOutboundLinkStates: async (section) => callBaseMethod( Podkop.AvailableMethods.GET_OUTBOUND_LINK_STATES, [section] ), getOutboundMetadata: async (section) => callBaseMethod( Podkop.AvailableMethods.GET_OUTBOUND_METADATA, [section] ), getSubscriptionMetadata: async (section) => callBaseMethod( Podkop.AvailableMethods.GET_SUBSCRIPTION_METADATA, [section] ), checkSingBox: async () => callBaseMethod( Podkop.AvailableMethods.CHECK_SING_BOX ), getSingBoxStatus: async () => callBaseMethod( Podkop.AvailableMethods.GET_SING_BOX_STATUS ), getZapretStatus: async () => callBaseMethod( Podkop.AvailableMethods.GET_ZAPRET_STATUS ), getByedpiStatus: async () => callBaseMethod( Podkop.AvailableMethods.GET_BYEDPI_STATUS ), getClashApiProxies: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ Podkop.AvailableClashAPIMethods.GET_PROXIES ]), getClashApiConnections: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ Podkop.AvailableClashAPIMethods.GET_CONNECTIONS ]), getClashApiProxyLatency: async (tag) => callBaseMethod( Podkop.AvailableMethods.CLASH_API, [Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, "5000"] ), getClashApiGroupLatency: async (tag) => callBaseMethod( Podkop.AvailableMethods.CLASH_API, [Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"] ), setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY, group, proxy ]), closeClashApiConnection: async (connectionId) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ Podkop.AvailableClashAPIMethods.CLOSE_CONNECTION, connectionId ]), closeAllClashApiConnections: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ Podkop.AvailableClashAPIMethods.CLOSE_ALL_CONNECTIONS ]), restart: async () => callBaseMethod( Podkop.AvailableMethods.RESTART, [], "/etc/init.d/podkop-plus" ), start: async () => callBaseMethod( Podkop.AvailableMethods.START, [], "/etc/init.d/podkop-plus" ), stop: async () => callBaseMethod( Podkop.AvailableMethods.STOP, [], "/etc/init.d/podkop-plus" ), enable: async () => callBaseMethod( Podkop.AvailableMethods.ENABLE, [], "/etc/init.d/podkop-plus" ), disable: async () => callBaseMethod( Podkop.AvailableMethods.DISABLE, [], "/etc/init.d/podkop-plus" ), globalCheck: async () => callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), showSingBoxConfig: async () => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), checkSingBoxLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_SING_BOX_LOGS), getSystemInfo: async () => callBaseMethod( Podkop.AvailableMethods.GET_SYSTEM_INFO ), componentAction: async (component, action, expectedLatestVersion) => { const startedAt = Date.now(); let selfUpdateVersionMatchedAt = 0; const startResponse = await executeShellCommand({ command: "/usr/bin/podkop-plus", args: [ Podkop.AvailableMethods.COMPONENT_ACTION_ASYNC, component, action ], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedStartResponse = parseComponentActionStartResult(startResponse); if ((startResponse.code ?? 0) !== 0 || !parsedStartResponse?.success || !parsedStartResponse.job_id) { return componentActionFailure(startResponse, parsedStartResponse); } const jobId = parsedStartResponse.job_id; while (Date.now() - startedAt < COMPONENT_ACTION_TIMEOUT_MS) { await sleep(COMPONENT_ACTION_POLL_INTERVAL_MS); const stateResponse = await readComponentActionState(jobId); if (stateResponse) { if (stateResponse.success === false) { return { success: false, error: stateResponse.message || _("Failed to execute") }; } if (stateResponse.running) { continue; } return { success: true, data: stateResponse }; } const statusResponse = await executeShellCommand({ command: "/usr/bin/podkop-plus", args: [ Podkop.AvailableMethods.COMPONENT_ACTION_STATUS, jobId ], timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS }); const parsedResponse = parseComponentActionResult(statusResponse); if (parsedResponse?.success === false) { return componentActionFailure(statusResponse, parsedResponse); } if ((statusResponse.code ?? 0) !== 0 || !parsedResponse) { if (component === "podkop" && action === "install") { const installedVersion = expectedLatestVersion ? await readPodkopVersion() : ""; 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: _("Podkop Plus has been installed"), current_version: installedVersion, latest_version: expectedLatestVersion, changed: true, status: "latest" } }; } } continue; } return componentActionFailure(statusResponse); } if (parsedResponse.running) { continue; } return { success: true, data: parsedResponse }; } return { success: false, error: _("Operation timed out") }; }, subscriptionUpdate: async (section, sourceIndex) => { const args = [ Podkop.AvailableMethods.SUBSCRIPTION_UPDATE, ...section ? [section] : [], ...section && sourceIndex !== void 0 ? [String(sourceIndex)] : [] ]; const response = await executeShellCommand({ command: "/usr/bin/podkop-plus", args, timeout: SUBSCRIPTION_UPDATE_TIMEOUT_MS }); if ((response.code ?? 0) !== 0) { return { success: false, error: response.stderr || _("Subscription update failed") }; } return { success: true, data: response.stdout }; } }; // src/podkop/methods/custom/getDashboardSections.ts var DASHBOARD_SECTION_CACHE_DIR = "/var/run/podkop-plus/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 typeof window !== "undefined" && typeof window.location?.hostname === "string" && 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 PodkopShellMethods.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 getManualProxyLinks(section) { return getListValues(section.selector_proxy_links); } function hasSubscriptionSources(section) { return getSubscriptionSourceCount(section) > 0; } function getSubscriptionSourceCount(section) { return getListValues(section.subscription_urls).length; } function isUrlTestEnabled(section) { return section.urltest_enabled === "1"; } function isUrlTestFilteringEnabled(section) { return ["exclude", "include", "mixed"].includes( section.urltest_filter_mode || "disabled" ); } function shouldHideFilteredUrlTestOutbounds(section) { return isUrlTestEnabled(section) && isUrlTestFilteringEnabled(section) && section.urltest_hide_filtered_outbounds === "1"; } function shouldUseProxyGroup(section) { return getManualProxyLinks(section).length > 0 || hasSubscriptionSources(section); } function getSectionProxyConfigType(section) { if (hasSubscriptionSources(section)) { return "subscription"; } if (isUrlTestEnabled(section) && shouldUseProxyGroup(section)) { return "urltest"; } if (getManualProxyLinks(section).length > 0) { 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 isUrlTestOutbound(outbound) { return outbound.type?.toLowerCase() === "urltest"; } function getLatencySortValue(outbound) { const latency = Number(outbound.latency); return Number.isFinite(latency) && latency > 0 ? latency : Number.POSITIVE_INFINITY; } function getOutboundSortBucket(outbound) { if (isUrlTestOutbound(outbound)) { return 0; } return getLatencySortValue(outbound) === Number.POSITIVE_INFINITY ? 2 : 1; } function sortOutboundsForDashboard(outbounds) { return outbounds.map((outbound, index) => ({ outbound, index })).sort((left, right) => { const leftBucket = getOutboundSortBucket(left.outbound); const rightBucket = getOutboundSortBucket(right.outbound); if (leftBucket !== rightBucket) { return leftBucket - rightBucket; } if (leftBucket === 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]) ); } 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 buildProxyGroupOutbounds(section, proxies, outboundMetadata, subscriptionCopyableCodes = /* @__PURE__ */ new Set()) { const sectionName = section[".name"]; const proxyByCode = getProxyEntryByCode(proxies); const selectorTag = getOutboundTagBySection(sectionName); const urltestTag = getOutboundTagBySection(`${sectionName}-urltest`); const selector = proxyByCode.get(selectorTag); const fallbackUrltest = proxyByCode.get(urltestTag); const manualLinkByCode = buildManualLinkByCode(section); const selectorCodes = selector?.value?.all ?? []; const urltestCodes = fallbackUrltest?.value?.all ?? []; const hideFilteredUrlTestOutbounds = shouldHideFilteredUrlTestOutbounds(section) && Boolean(fallbackUrltest?.code) && urltestCodes.length > 0; const groupCodes = hideFilteredUrlTestOutbounds ? [fallbackUrltest?.code || "", ...urltestCodes] : selectorCodes.length ? selectorCodes : [fallbackUrltest?.code || "", ...urltestCodes]; const outbounds = uniqueCodes(groupCodes).flatMap((code) => { const item = proxyByCode.get(code); if (!item) { return []; } const isFastest = item.code === urltestTag; const link = manualLinkByCode.get(item.code) || ""; const canCopyLink = isCopyableProxyLink(link) || subscriptionCopyableCodes.has(item.code); return [ { code: item.code, displayName: isFastest ? _("Fastest") : getProxyUrlName(link) || outboundMetadata?.names?.[item.code] || item.value.name || item.code, latency: item.value.history?.[0]?.delay || 0, type: item.value.type || "", selected: selector?.value?.now === item.code, link, canCopyLink, country: outboundMetadata?.countries?.[item.code] } ]; }); return { selector, latencyTestCode: hideFilteredUrlTestOutbounds ? fallbackUrltest?.code : selector?.code, outbounds: sortOutboundsForDashboard(outbounds) }; } 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 getSubscriptionMetadata(sectionName, 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(sectionName, 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; } async function getDashboardSections(options = {}) { const includeSubscriptionCopyState = options.includeSubscriptionCopyState ?? true; const configSections = 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" && ["proxy", "outbound", "vpn", "byedpi"].includes( getSectionAction(section) ) ).map(async (section) => { const displayName = getDisplayName(section); const sectionName = section[".name"]; const sectionAction = getSectionAction(section); const proxyConfigType = getSectionProxyConfigType(section); if (sectionAction === "vpn") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find( (proxy) => proxy.code === outboundTag ); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, 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 } ] }; } if (sectionAction === "byedpi") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find( (proxy) => proxy.code === outboundTag ); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, outbounds: [ { code: outbound?.code || sectionName, displayName: "ByeDPI", latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false } ] }; } if (sectionAction === "outbound") { const outboundTag = getOutboundTagBySection(sectionName); const outbound = proxies.find( (proxy) => proxy.code === outboundTag ); return { withTagSelect: false, code: outbound?.code || sectionName, sectionName, displayName, outbounds: [ { code: outbound?.code || sectionName, displayName: getJsonOutboundDisplayName(section) || outbound?.value?.name || "", latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true, canCopyLink: false } ] }; } if (sectionAction === "proxy" && shouldUseProxyGroup(section)) { const subscriptionSourceCount = getSubscriptionSourceCount(section); const subscriptionEnabled = subscriptionSourceCount > 0; const dashboardCache = await readDashboardSectionCache(sectionName); const outboundMetadata = getOutboundMetadata(dashboardCache); const subscriptionMetadata = subscriptionEnabled ? getSubscriptionMetadata( sectionName, subscriptionSourceCount, dashboardCache ) : void 0; const subscriptionCopyableCodes = includeSubscriptionCopyState ? getSubscriptionCopyableCodes(dashboardCache) : /* @__PURE__ */ new Set(); const { selector, latencyTestCode, outbounds } = buildProxyGroupOutbounds( section, proxies, outboundMetadata, subscriptionCopyableCodes ); return { withTagSelect: true, code: selector?.code || sectionName, sectionName, displayName, latencyTestCode, proxyConfigType, subscriptionSourceCount, subscriptionMetadata, outbounds }; } return { withTagSelect: false, code: sectionName, sectionName, displayName, outbounds: [] }; }) ); return { success: true, data }; } // src/podkop/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/podkop/methods/custom/index.ts var CustomPodkopMethods = { getConfigSections, getDashboardSections, getClashApiSecret: getClashApiSecret2 }; // src/podkop/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/podkop/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/podkop/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/podkop/methods/fakeip/index.ts var RemoteFakeIPMethods = { getFakeIpCheck, getIpCheck }; // src/podkop/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/podkop/tabs/diagnostic/helpers/getCheckTitle.ts function getCheckTitle(name) { return `${name} ${_("checks")}`; } // src/podkop/tabs/diagnostic/checks/contstants.ts 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: 3, title: getCheckTitle("Nftables"), code: "NFT" /* NFT */ }, ["ZAPRET" /* ZAPRET */]: { order: 4, title: getCheckTitle("Zapret"), code: "ZAPRET" /* ZAPRET */ }, ["BYEDPI" /* BYEDPI */]: { order: 5, title: getCheckTitle("ByeDPI"), code: "BYEDPI" /* BYEDPI */ }, ["OUTBOUNDS" /* OUTBOUNDS */]: { order: 6, title: getCheckTitle("Outbounds"), code: "OUTBOUNDS" /* OUTBOUNDS */ }, ["FAKEIP" /* FAKEIP */]: { order: 7, title: getCheckTitle("FakeIP"), code: "FAKEIP" /* FAKEIP */ } }; // src/podkop/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 */, "NFT" /* NFT */ ]; if (options.includeZapret) { checks.push("ZAPRET" /* ZAPRET */); } 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, podkop_version: "loading", podkop_latest_version: "loading", luci_app_version: "loading", sing_box_version: "loading", sing_box_extended: 0, zapret_version: "loading", zapret_installed: 0, byedpi_version: "loading", byedpi_installed: 0, 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: { podkopCheck: { loading: false }, podkopInstall: { loading: false }, singBoxCheck: { loading: false }, singBoxInstall: { loading: false }, singBoxInstallExtended: { loading: false }, singBoxInstallStable: { loading: false }, zapretCheck: { loading: false }, zapretInstall: { loading: false }, zapretRemove: { loading: false }, byedpiCheck: { loading: false }, byedpiInstall: { loading: false }, byedpiRemove: { loading: false } }, updatesChecks: { podkop: { status: null, latest_version: "", release_url: "" }, sing_box: { status: null, latest_version: "", release_url: "" }, zapret: { status: null, latest_version: "", release_url: "" }, byedpi: { status: null, latest_version: "", release_url: "" } } }; // src/podkop/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, podkopRunning: 0, podkopEnabled: 0, podkopStatus: "" } }, sectionsWidget: { loading: true, failed: false, latencyFetchingSections: {}, selectorSwitchingSections: {}, subscriptionUpdatingSections: {}, data: [] }, ...initialDiagnosticStore }; var store = new StoreService(initialStore); // src/podkop/services/podkopLogWatcher.service.ts var PodkopLogWatcher = class _PodkopLogWatcher { constructor() { this.intervalMs = 5e3; this.lastLines = []; this.suppressInitialLogs = false; this.initialized = false; 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 (!_PodkopLogWatcher.instance) { _PodkopLogWatcher.instance = new _PodkopLogWatcher(); } return _PodkopLogWatcher.instance; } init(fetcher, options) { this.fetcher = fetcher; this.onNewLog = options?.onNewLog; this.intervalMs = options?.intervalMs ?? 5e3; this.suppressInitialLogs = options?.suppressInitialLogs ?? false; this.maxTrackedLines = options?.maxTrackedLines ?? 500; this.lastLines = []; this.initialized = false; logger.info( "[PodkopLogWatcher]", `initialized (interval: ${this.intervalMs}ms)` ); } normalizeLines(raw) { return raw.split("\n").filter(Boolean).slice(-this.maxTrackedLines); } findOverlapLength(lines) { const maxOverlap = Math.min(this.lastLines.length, lines.length); for (let length = maxOverlap; length > 0; length--) { let matches = true; const previousStart = this.lastLines.length - length; for (let index = 0; index < length; index++) { if (this.lastLines[previousStart + index] !== lines[index]) { matches = false; break; } } if (matches) { return length; } } return 0; } async checkOnce() { if (!this.fetcher) { logger.warn("[PodkopLogWatcher]", "fetcher not found"); return; } if (this.paused) { logger.debug("[PodkopLogWatcher]", "skipped check \u2014 tab not visible"); return; } if (this.checking) { logger.debug( "[PodkopLogWatcher]", "skipped check \u2014 previous check is running" ); return; } this.checking = true; try { const raw = await this.fetcher(); const lines = this.normalizeLines(raw); if (!this.initialized) { this.initialized = true; this.lastLines = lines; if (this.suppressInitialLogs) { return; } for (const line of lines) { this.onNewLog?.(line); } return; } const overlapLength = this.findOverlapLength(lines); const newLines = this.lastLines.length ? lines.slice(overlapLength) : lines; for (const line of newLines) { this.onNewLog?.(line); } this.lastLines = lines; } catch (err) { logger.error("[PodkopLogWatcher]", "failed to read logs:", err); } finally { this.checking = false; } } start() { if (this.running) return; if (!this.fetcher) { logger.warn("[PodkopLogWatcher]", "attempted to start without fetcher"); return; } this.running = true; void this.checkOnce(); this.timer = setInterval(() => this.checkOnce(), this.intervalMs); logger.info( "[PodkopLogWatcher]", `started (interval: ${this.intervalMs}ms)` ); } stop() { if (!this.running) return; this.running = false; if (this.timer) clearInterval(this.timer); logger.info("[PodkopLogWatcher]", "stopped"); } pause() { if (!this.running || this.paused) return; this.paused = true; logger.info("[PodkopLogWatcher]", "paused (tab not visible)"); } resume() { if (!this.running || !this.paused) return; this.paused = false; logger.info("[PodkopLogWatcher]", "resumed (tab active)"); this.checkOnce(); } reset() { this.lastLines = []; this.initialized = false; this.checking = false; logger.info("[PodkopLogWatcher]", "log history reset"); } }; // src/podkop/services/core.service.ts var LOG_NOTIFICATION_DEDUPE_WINDOW_MS = 15e3; var recentErrorNotifications = /* @__PURE__ */ new Map(); var activeErrorNotifications = /* @__PURE__ */ new Map(); function isErrorLogLine(line) { const lower = line.toLowerCase(); return lower.includes("[error]") || lower.includes("[fatal]"); } function isLogLifecycleBoundary(line) { const lower = line.toLowerCase(); return lower.includes("[info] starting podkop plus") || lower.includes("[info] stopping podkop plus") || lower.includes("[info] podkop plus reload") || lower.includes("[info] podkop plus restart"); } function getNotificationKey(line) { const lower = line.toLowerCase(); const errorIndex = lower.indexOf("[error]"); const fatalIndex = lower.indexOf("[fatal]"); const markerIndex = errorIndex >= 0 && fatalIndex >= 0 ? Math.min(errorIndex, fatalIndex) : Math.max(errorIndex, fatalIndex); return (markerIndex >= 0 ? line.slice(markerIndex) : line).trim(); } function shouldNotifyAboutLogLine(line) { const key = getNotificationKey(line); const now = Date.now(); const lastShownAt = recentErrorNotifications.get(key) ?? 0; recentErrorNotifications.forEach((shownAt, storedKey) => { if (now - shownAt > LOG_NOTIFICATION_DEDUPE_WINDOW_MS) { recentErrorNotifications.delete(storedKey); } }); if (now - lastShownAt < LOG_NOTIFICATION_DEDUPE_WINDOW_MS) { return false; } recentErrorNotifications.set(key, now); return true; } function removeNotification(notification) { if (!notification.parentNode) { return; } notification.classList.add("fade-out"); notification.classList.remove("fade-in"); setTimeout(() => notification.remove(), 500); } function clearLogErrorNotifications() { activeErrorNotifications.forEach(removeNotification); activeErrorNotifications.clear(); recentErrorNotifications.clear(); } function showLogErrorNotification(line) { const key = getNotificationKey(line); const existingNotification = activeErrorNotifications.get(key); if (existingNotification) { removeNotification(existingNotification); } const notification = ui.addNotification( _("Podkop Plus Error"), E("div", {}, line), "error", "pdk-log-error-notification" ); activeErrorNotifications.set(key, notification); } function coreService() { TabServiceInstance.onChange((activeId, tabs) => { logger.info("[TAB]", activeId); store.set({ tabService: { current: activeId || "", all: tabs.map((tab) => tab.id) } }); }); const watcher = PodkopLogWatcher.getInstance(); watcher.init( async () => { const logs = await PodkopShellMethods.checkLogs(); if (logs.success) { return logs.data; } return ""; }, { intervalMs: 3e3, suppressInitialLogs: true, onNewLog: (line) => { if (isLogLifecycleBoundary(line)) { clearLogErrorNotifications(); } if (isErrorLogLine(line) && shouldNotifyAboutLogLine(line)) { showLogErrorNotification(line); } } } ); watcher.start(); } // src/podkop/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/podkop/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 [podkopResult, singboxResult] = await Promise.allSettled([ PodkopShellMethods.getStatus(), PodkopShellMethods.getSingBoxStatus() ]); if (requestId !== latestServicesInfoRequestId) { return; } const podkop = getSettledMethodResponse("getStatus", podkopResult); const singbox = getSettledMethodResponse("getSingBoxStatus", singboxResult); store.set({ servicesInfoWidget: { loading: false, failed: !podkop.success || !singbox.success, data: { singbox: singbox.success ? singbox.data.running : 0, podkopRunning: podkop.success ? podkop.data.running : 0, podkopEnabled: podkop.success ? podkop.data.enabled : 0, podkopStatus: podkop.success ? podkop.data.status : "" } } }); } // src/podkop/tabs/dashboard/initController.ts var SECTIONS_REFRESH_INTERVAL_MS = 1e4; var sectionsRefreshTimer = null; var sectionsRefreshPromise = null; var sectionsRefreshQueued = false; var dashboardMounted = false; var dashboardMountId = 0; async function fetchDashboardSectionsOnce(mountId) { 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 CustomPodkopMethods.getDashboardSections(); if (!dashboardMounted || mountId !== dashboardMountId) { 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) { 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) { 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) { const sectionsWidget = store.get().sectionsWidget; const latencyFetchingSections = { ...sectionsWidget.latencyFetchingSections }; if (fetching) { latencyFetchingSections[sectionName] = true; } else { delete latencyFetchingSections[sectionName]; } store.set({ sectionsWidget: { ...sectionsWidget, latencyFetchingSections } }); } async function connectToClashSockets() { const clashApiSecret = await getClashApiSecret2(); socket.subscribe( `${getClashWsUrl()}/traffic?token=${clashApiSecret}`, (msg) => { const parsedMsg = JSON.parse(msg); store.set({ bandwidthWidget: { loading: false, failed: false, data: { up: parsedMsg.up, down: parsedMsg.down } } }); }, (_err) => { 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) => { 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) => { 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 } } }); } ); } 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 PodkopShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections({ force: true }); } finally { setSelectorSwitching(sectionName); } } async function handleTestGroupLatency(sectionName, tag) { if (store.get().sectionsWidget.latencyFetchingSections[sectionName]) { return; } setLatencyFetching(sectionName, true); try { await PodkopShellMethods.getClashApiGroupLatency(tag); await fetchDashboardSections({ force: true }); } finally { setLatencyFetching(sectionName, false); } } async function handleTestProxyLatency(sectionName, tag) { if (store.get().sectionsWidget.latencyFetchingSections[sectionName]) { return; } setLatencyFetching(sectionName, true); try { await PodkopShellMethods.getClashApiProxyLatency(tag); await fetchDashboardSections({ force: true }); } finally { setLatencyFetching(sectionName, false); } } async function handleCopyOutbound(section, outbound) { const link = outbound.link; if (link && isCopyableProxyLink(link)) { copyToClipboard(link); return; } const response = await PodkopShellMethods.getOutboundLink( section.sectionName, outbound.code ); if (response.success && isCopyableProxyLink(response.data.link)) { copyToClipboard(response.data.link); return; } showToast(_("Proxy link is unavailable"), "error"); } async function handleUpdateSubscription(section) { if (store.get().sectionsWidget.subscriptionUpdatingSections[section.sectionName]) { return; } setSubscriptionUpdating(section.sectionName, true); try { const response = await PodkopShellMethods.subscriptionUpdate( section.sectionName ); if (!response.success) { setSubscriptionUpdating(section.sectionName, false); showToast(_("Failed to update subscriptions"), "error"); return; } setSubscriptionUpdating(section.sectionName, false); showToast(_("Subscription update completed"), "success"); void fetchDashboardSections({ force: true }); void fetchServicesInfo(); } catch (error) { logger.error("[DASHBOARD]", "handleUpdateSubscription: failed", error); setSubscriptionUpdating(section.sectionName, false); showToast(_("Failed to update subscriptions"), "error"); } } 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: () => { }, onUpdateSubscription: () => { }, latencyFetching: false, 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] ), subscriptionUpdating: Boolean( sectionsWidget.subscriptionUpdatingSections[section.sectionName] ), selectorSwitchingTag: sectionsWidget.selectorSwitchingSections[section.sectionName], onTestLatency: (tag) => { if (section.withTagSelect) { return handleTestGroupLatency(section.sectionName, tag); } return handleTestProxyLatency(section.sectionName, tag); }, onChooseOutbound: (sectionName, selector, tag) => { void handleChooseOutbound(sectionName, selector, tag); }, onCopyOutbound: (section2, outbound) => { void handleCopyOutbound(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: "Podkop Plus", value: servicesInfoWidget.data.podkopRunning ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: servicesInfoWidget.data.podkopRunning ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" } }, { key: "Sing-box", value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), attributes: { class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" } } ] }); container.replaceChildren(renderedWidget); } async function onStoreUpdate(_next, _prev, diff) { if (diff.sectionsWidget) { renderSectionsWidget(); } if (diff.bandwidthWidget) { renderBandwidthWidget(); } if (diff.trafficTotalWidget) { renderTrafficTotalWidget(); } if (diff.systemInfoWidget) { renderSystemInfoWidget(); } if (diff.servicesInfoWidget) { renderServicesInfoWidget(); } } async function onPageMount() { onPageUnmount(); dashboardMounted = true; dashboardMountId += 1; store.subscribe(onStoreUpdate); void fetchDashboardSections({ force: true }); void fetchServicesInfo(); void connectToClashSockets(); sectionsRefreshTimer = setInterval(() => { void fetchDashboardSections(); }, SECTIONS_REFRESH_INTERVAL_MS); } function onPageUnmount() { dashboardMounted = false; dashboardMountId += 1; if (sectionsRefreshTimer) { clearInterval(sectionsRefreshTimer); sectionsRefreshTimer = null; } sectionsRefreshQueued = false; sectionsRefreshPromise = null; store.unsubscribe(onStoreUpdate); store.reset([ "bandwidthWidget", "trafficTotalWidget", "systemInfoWidget", "servicesInfoWidget", "sectionsWidget" ]); socket.resetAll(); } 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"); onPageMount(); registerLifecycleListeners(); }); } // src/podkop/tabs/dashboard/styles.ts var styles = ` #cbi-${PODKOP_UCI_PACKAGE}-dashboard-_mount_node > div { width: 100%; } #cbi-${PODKOP_UCI_PACKAGE}-dashboard > h3 { display: none; } .pdk_dashboard-page { width: 100%; --dashboard-grid-columns: 4; --dashboard-grid-min-width: 180px; } @media (max-width: 900px) { .pdk_dashboard-page { --dashboard-grid-columns: 2; } } @media (max-width: 560px) { .pdk_dashboard-page { --dashboard-grid-columns: 1; --dashboard-grid-min-width: 0; } } .pdk_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; } .pdk_dashboard-page__widgets-section__item { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; min-width: 0; } .pdk_dashboard-page__widgets-section__item__title {} .pdk_dashboard-page__widgets-section__item__row {} .pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { color: var(--success-color-medium, green); } .pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { color: var(--error-color-medium, red); } .pdk_dashboard-page__widgets-section__item__row__key {} .pdk_dashboard-page__widgets-section__item__row__value {} .pdk_dashboard-page__outbound-section { margin-top: 10px; border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; } .pdk_dashboard-page__outbound-section__title-section { display: flex; align-items: center; justify-content: space-between; gap: 8px 10px; min-width: 0; } .pdk_dashboard-page__outbound-section__title-section__title { color: var(--text-color-high); font-weight: 700; min-width: 0; overflow-wrap: anywhere; } .pdk_dashboard-page__outbound-section__title-section__actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex: 0 0 auto; } .pdk_dashboard-page .btn.pdk_dashboard-page__outbound-section__subscription-update { width: 28px; height: 28px; min-width: 28px; min-height: 28px; padding: 2px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; line-height: 1; margin: 0; } .pdk_dashboard-page__outbound-section__subscription-update svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .pdk_dashboard-page__outbound-section__subscription-update[disabled] { cursor: not-allowed; opacity: 0.65; } .pdk_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; } .pdk_dashboard-page .btn.dashboard-sections-grid-item-test-latency svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .pdk_dashboard-page .btn.dashboard-sections-grid-item-test-latency[disabled] { cursor: not-allowed; opacity: 0.65; } .pdk_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; } .pdk_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); } .pdk_dashboard-page__subscription-meta__main { display: flex; align-items: center; gap: 6px 10px; min-width: 0; } .pdk_dashboard-page__subscription-meta__heading { flex: 0 0 auto; color: var(--text-color-high); font-weight: 700; line-height: 1.25; white-space: nowrap; } .pdk_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; } .pdk_dashboard-page__subscription-meta__facts { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 5px 12px; } .pdk_dashboard-page__subscription-meta__fact { display: flex; align-items: baseline; gap: 4px; min-width: 0; line-height: 1.25; } .pdk_dashboard-page__subscription-meta__fact-key { color: var(--text-color-medium); font-size: 12px; white-space: nowrap; } .pdk_dashboard-page__subscription-meta__fact-value { color: var(--text-color-high); font-weight: 600; overflow-wrap: anywhere; } .pdk_dashboard-page__subscription-meta__actions { flex: 0 0 auto; margin-left: auto; display: flex; justify-content: flex-end; gap: var(--subscription-meta-action-gap); } .pdk_dashboard-page .btn.pdk_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; } .pdk_dashboard-page__subscription-meta__action svg { width: 15px; height: 15px; display: block; flex: 0 0 auto; } .pdk_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) { .pdk_dashboard-page__subscription-meta__main { align-items: flex-start; flex-wrap: wrap; } .pdk_dashboard-page__subscription-meta__heading, .pdk_dashboard-page__subscription-meta__title { order: 1; } .pdk_dashboard-page__subscription-meta__actions { order: 2; } .pdk_dashboard-page__subscription-meta__facts { order: 3; flex-basis: 100%; } .pdk_dashboard-page__subscription-meta__title { max-width: calc(100% - 42px); } } .pdk_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; } .pdk_dashboard-page__outbound-grid__item--selectable { cursor: pointer; } .pdk_dashboard-page__outbound-grid__item--selectable:hover { border-color: var(--primary-color-high, dodgerblue); } .pdk_dashboard-page__outbound-grid__item--active { border-color: var(--success-color-medium, green); } .pdk_dashboard-page__outbound-grid__item--disabled { cursor: default; } .pdk_dashboard-page__outbound-grid__item--switching { border-color: transparent !important; overflow: hidden; cursor: wait; } .pdk_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; } .pdk_dashboard-page__outbound-grid__item__snake rect { stroke: var(--primary-color-high, dodgerblue); stroke-width: 4; animation: pdk-dashboard-selector-snake-svg 1.2s linear infinite; } @keyframes pdk-dashboard-selector-snake-svg { 0% { stroke-dasharray: 30 70; stroke-dashoffset: 100; } 100% { stroke-dasharray: 30 70; stroke-dashoffset: 0; } } .pdk_dashboard-page__outbound-grid__item__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; } .pdk_dashboard-page__outbound-grid__item__header b { min-width: 0; line-height: 1.25; overflow-wrap: anywhere; } .pdk_dashboard-page .btn.pdk_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; } .pdk_dashboard-page__outbound-grid__item__copy-button svg { width: 13px; height: 13px; display: block; flex: 0 0 auto; } .pdk_dashboard-page__outbound-grid__item__footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; } .pdk_dashboard-page__outbound-grid__item__type { min-width: 0; overflow-wrap: anywhere; } .pdk_dashboard-page__outbound-grid__item__latency--empty { color: var(--primary-color-low, lightgray); } .pdk_dashboard-page__outbound-grid__item__latency--green { color: var(--success-color-medium, green); } .pdk_dashboard-page__outbound-grid__item__latency--yellow { color: var(--warn-color-medium, orange); } .pdk_dashboard-page__outbound-grid__item__latency--red { color: var(--error-color-medium, red); } `; // src/podkop/tabs/dashboard/index.ts var DashboardTab = { render, initController, styles }; // src/podkop/tabs/diagnostic/renderDiagnostic.ts function render2() { return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [ E("div", { class: "pdk_diagnostic-page__left-bar" }, [ E("div", { id: "pdk_diagnostic-page-run-check" }), E("div", { class: "pdk_diagnostic-page__checks", id: "pdk_diagnostic-page-checks" }) ]), E("div", { class: "pdk_diagnostic-page__right-bar" }, [ E("div", { id: "pdk_diagnostic-page-wiki" }), E("div", { id: "pdk_diagnostic-page-actions" }), E("div", { id: "pdk_diagnostic-page-system-info" }) ]) ]); } // src/podkop/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/podkop/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/podkop/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 PodkopShellMethods.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 allGood = Boolean(data.dns_on_router) && Boolean(data.dhcp_config_status) && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status); const atLeastOneGood = Boolean(data.dns_on_router) || Boolean(data.dhcp_config_status) || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status); const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, code, title, description, state, items: [ ...insertIf( data.dns_type === "doh" || data.dns_type === "dot" || !data.bootstrap_dns_status, [ { state: data.bootstrap_dns_status ? "success" : "error", key: _("Bootsrap DNS"), value: data.bootstrap_dns_server } ] ), { state: data.dns_status ? "success" : "error", key: _("Main DNS"), value: `${data.dns_server} [${data.dns_type}]` }, { state: data.dns_on_router ? "success" : "error", key: _("DNS on router"), value: "" }, { state: data.dhcp_config_status ? "success" : "error", key: _("DHCP has DNS server"), value: "" } ] }); if (!atLeastOneGood) { throw new Error("DNS checks failed"); } } // src/podkop/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 PodkopShellMethods.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/podkop/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 PodkopShellMethods.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/podkop/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 PodkopShellMethods.checkFakeIP(); const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); const checks = { singBoxFakeIP: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, browserFakeIP: checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, differentIP: checkFakeIPResponse.success && checkIPResponse.success && checkFakeIPResponse.data.IP !== checkIPResponse.data.IP }; const allGood = checks.singBoxFakeIP && checks.browserFakeIP && checks.differentIP; const atLeastOneGood = checks.singBoxFakeIP || checks.browserFakeIP || checks.differentIP; const { state, description } = getMeta({ atLeastOneGood, allGood }); 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: checks.browserFakeIP ? "success" : "error", key: checks.browserFakeIP ? _("Browser is using FakeIP correctly") : _("Browser is not using FakeIP"), value: "" }, ...insertIf(checks.browserFakeIP, [ { state: checks.differentIP ? "success" : "error", key: checks.differentIP ? _("Proxy traffic is routed via FakeIP") : _("Proxy traffic is not routed via FakeIP"), value: "" } ]) ] }); } // src/podkop/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 PodkopShellMethods.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 ready = Boolean(data.ready); 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 podkopRuntimeReady = !hasZapretRules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasZapretRules && (runningProcesses > 0 || supervisorProcesses > 0); let state = "success"; let description = _("Checks passed"); if (hasZapretRules && !providerAvailable) { state = "error"; description = _("Checks failed"); } else if (hasZapretRules && !ready) { state = "error"; description = _("Checks failed"); } else if (queueOverlap) { state = "error"; description = _("Checks failed"); } else if (!hasZapretRules && !providerAvailable) { state = "warning"; description = _("Issues detected"); } else if (standaloneConflict) { state = "warning"; description = _("Issues detected"); } const items = [ { state: providerAvailable ? "success" : hasZapretRules ? "error" : "warning", key: providerAvailable ? _("Zapret provider binary is available") : _("Zapret provider binary is not available"), value: "" }, { 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 || !podkopRuntimeReady ? "error" : "success", key: hasZapretRules ? podkopRuntimeReady ? _("Podkop Plus-managed nfqws runtime is ready") : _("Podkop Plus-managed nfqws runtime is not ready") : unexpectedRuntime ? _("Unexpected Podkop Plus-managed nfqws runtime is running") : _("Podkop Plus-managed nfqws runtime is not running"), value: "" }, { 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: standaloneConflict ? "warning" : "success", key: standaloneServiceRunning ? hasZapretRules ? _( "Standalone Zapret is active together with Podkop Plus Zapret rules" ) : _("Standalone Zapret service is active") : _("Standalone Zapret service is inactive"), value: "" } ]; updateCheckStore({ order, code, title, description, state, items }); } // src/podkop/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 PodkopShellMethods.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 ready = Boolean(data.ready); 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 podkopRuntimeReady = !hasByedpiRules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasByedpiRules && (runningProcesses > 0 || supervisorProcesses > 0); const outboundsConfigured = Boolean(data.outbounds_configured); const routesConfigured = Boolean(data.routes_configured); const standaloneServiceEnabled = Boolean(data.standalone_service_enabled); const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneConflict = hasByedpiRules && standaloneServiceRunning; const standaloneAutostartRisk = hasByedpiRules && standaloneServiceEnabled && !standaloneServiceRunning; let state = "success"; let description = _("Checks passed"); if (hasByedpiRules && !providerAvailable) { state = "error"; description = _("Checks failed"); } else if (hasByedpiRules && !ready) { state = "error"; description = _("Checks failed"); } else if (standaloneConflict) { state = "error"; description = _("Checks failed"); } else if (runtimeUnstable) { state = "warning"; description = _("Issues detected"); } else if (standaloneAutostartRisk) { state = "warning"; description = _("Issues detected"); } else if (!packageInstalled) { state = "warning"; description = _("Issues detected"); } 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 || !podkopRuntimeReady ? "error" : runtimeUnstable ? "warning" : "success", key: hasByedpiRules ? runtimeUnstable ? _("Podkop Plus-managed ciadpi runtime has restarted") : podkopRuntimeReady ? _("Podkop Plus-managed ciadpi runtime is ready") : _("Podkop Plus-managed ciadpi runtime is not ready") : unexpectedRuntime ? _("Unexpected Podkop Plus-managed ciadpi runtime is running") : _("Podkop Plus-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: !hasByedpiRules || routesConfigured ? "success" : "error", key: routesConfigured ? _("ByeDPI route rules are configured") : _("ByeDPI route rules are not configured"), value: "" }, { state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success", key: standaloneServiceRunning ? hasByedpiRules ? _( "Standalone ByeDPI is active together with Podkop Plus ByeDPI rules" ) : _("Standalone ByeDPI service is active") : standaloneAutostartRisk ? _("Standalone ByeDPI autostart is enabled") : _("Standalone ByeDPI service is inactive"), value: "" } ]; updateCheckStore({ order, code, title, description, state, items }); } // src/podkop/services/systemInfo.service.ts var UNKNOWN_SYSTEM_INFO = { loading: false, loaded: false, providerInfoLoaded: false, podkop_version: _("unknown"), podkop_latest_version: _("unknown"), luci_app_version: _("unknown"), sing_box_version: _("unknown"), sing_box_extended: 0, zapret_version: _("unknown"), zapret_installed: 0, byedpi_version: _("unknown"), byedpi_installed: 0, 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 PodkopShellMethods.getSystemInfo(); if (requestId !== latestSystemInfoRequestId) { return store.get().diagnosticsSystemInfo; } if (systemInfo.success) { const nextSystemInfo = { ...UNKNOWN_SYSTEM_INFO, loading: false, loaded: true, providerInfoLoaded: true, ...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, byedpi_installed: latestSystemInfo.byedpi_installed }; 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 = ` .pdk-partial-button { text-align: center; } .pdk-partial-button--with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 5px; } .pdk-partial-button--loading { } .pdk-partial-button--disabled { } .pdk-partial-button__icon { flex: 0 0 auto; } .pdk-partial-button__icon { display: inline-flex; align-items: center; justify-content: center; } .pdk-partial-button__icon svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } `; // src/partials/modal/styles.ts var styles3 = ` .pdk-partial-modal__body {} .pdk-partial-modal__content { max-height: 70vh; overflow: scroll; border-radius: 4px; } .pdk-partial-modal__footer { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 10px; } .pdk-partial-modal__footer button { margin-left: 0; } .pdk-partial-modal__checkbox { display: inline-flex; align-items: center; gap: 8px; margin-right: auto; cursor: pointer; user-select: none; } .pdk-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: "pdk-partial-button__icon" }); if (loading2) { iconWrap.appendChild(renderLoaderCircleIcon24()); return iconWrap; } if (icon) { iconWrap.appendChild(icon()); return iconWrap; } return iconWrap; } function getClass() { return [ "btn", "pdk-partial-button", ...insertIf(Boolean(disabled), ["pdk-partial-button--disabled"]), ...insertIf(Boolean(loading2), ["pdk-partial-button--loading"]), ...insertIf(Boolean(hasIcon), ["pdk-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 currentText = text ?? ""; let refreshInFlight = false; let pendingRefresh = false; let refreshSessionId = 0; let timer; let observer; let autoRefreshEnabled = options?.initialAutoRefresh ?? Boolean(options?.getText); let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd); let autoRefreshInput; const codeEl = E("code", {}, currentText); const contentEl = E( "pre", { class: "pdk-partial-modal__content" }, codeEl ); const stopRefreshTimer = () => { if (timer) { clearInterval(timer); timer = void 0; } }; const destroyLiveRefresh = () => { refreshSessionId += 1; pendingRefresh = 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 refreshText = async () => { if (!options?.getText || !autoRefreshEnabled || refreshInFlight) { return; } if (!body.isConnected) { return; } refreshInFlight = true; const sessionId = refreshSessionId; try { const nextText = await options.getText(); if (!body.isConnected || !autoRefreshEnabled) { return; } if (sessionId !== refreshSessionId) { return; } updateText(nextText ?? ""); } catch (error) { console.warn("[renderModal] failed to refresh modal content", error); } finally { refreshInFlight = false; if (pendingRefresh) { pendingRefresh = false; if (autoRefreshEnabled && body.isConnected) { void refreshText(); } } } }; const requestRefresh = () => { if (!options?.getText || !autoRefreshEnabled) { return; } if (refreshInFlight) { pendingRefresh = true; return; } void refreshText(); }; 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; if (autoRefreshInput) { autoRefreshInput.checked = nextValue; } if (nextValue) { startRefreshTimer(); requestRefresh(); return; } stopRefreshTimer(); }; 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: "pdk-partial-modal__checkbox" }, [ autoRefreshInput, E( "span", { class: "pdk-partial-modal__checkbox-text" }, options.autoRefreshLabel ?? _("Auto refresh") ) ]) ); } const body = E("div", { class: "pdk-partial-modal__body" }, [ E("div", {}, [ contentEl, E("div", { class: "pdk-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(); } return body; } // src/partials/index.ts var PartialStyles = ` ${styles2} ${styles3} `; // src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts function renderAvailableActions({ restart, start, stop, enable, disable, globalCheck, viewLogs, showSingBoxConfig }) { return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), ...insertIf(restart.visible, [ renderButton({ classNames: ["cbi-button-apply"], onClick: restart.onClick, icon: renderRotateCcwIcon24, text: _("Restart Podkop Plus"), loading: restart.loading, disabled: restart.disabled }) ]), ...insertIf(stop.visible, [ renderButton({ classNames: ["cbi-button-remove"], onClick: stop.onClick, icon: renderCircleStopIcon24, text: _("Stop Podkop Plus"), loading: stop.loading, disabled: stop.disabled }) ]), ...insertIf(start.visible, [ renderButton({ classNames: ["cbi-button-save"], onClick: start.onClick, icon: renderCirclePlayIcon24, text: _("Start Podkop Plus"), 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/podkop/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: "pdk_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: `pdk_diagnostic_alert__summary__item pdk_diagnostic_alert__summary__item--${item.state}` }, [getIcon(), E("b", {}, item.key), E("div", {}, item.value)] ); }); return E("div", { class: "pdk_diagnostic_alert__summary" }, renderedItems); } function renderLoadingState3(props) { const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); iconWrap.appendChild(renderLoaderCircleIcon24()); return E( "div", { class: "pdk_diagnostic_alert pdk_diagnostic_alert--loading" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ E("b", { class: "pdk_diagnostic_alert__title" }, props.title), E( "div", { class: "pdk_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderWarningState(props) { const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleAlertIcon24()); return E( "div", { class: "pdk_diagnostic_alert pdk_diagnostic_alert--warning" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ E("b", { class: "pdk_diagnostic_alert__title" }, props.title), E( "div", { class: "pdk_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderErrorState(props) { const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleXIcon24()); return E( "div", { class: "pdk_diagnostic_alert pdk_diagnostic_alert--error" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ E("b", { class: "pdk_diagnostic_alert__title" }, props.title), E( "div", { class: "pdk_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSuccessState(props) { const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleCheckIcon24()); return E( "div", { class: "pdk_diagnostic_alert pdk_diagnostic_alert--success" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ E("b", { class: "pdk_diagnostic_alert__title" }, props.title), E( "div", { class: "pdk_diagnostic_alert__description" }, props.description ) ]), E("div", {}, ""), renderCheckSummary(props.items) ] ); } function renderSkippedState(props) { const iconWrap = E("span", { class: "pdk_diagnostic_alert__icon" }); iconWrap.appendChild(renderCircleSlashIcon24()); return E( "div", { class: "pdk_diagnostic_alert pdk_diagnostic_alert--skipped" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ E("b", { class: "pdk_diagnostic_alert__title" }, props.title), E( "div", { class: "pdk_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/podkop/tabs/diagnostic/partials/renderRunAction.ts function renderRunAction({ loading: loading2, disabled, click }) { return E("div", { class: "pdk_diagnostic-page__run_check_wrapper" }, [ renderButton({ text: _("Run Diagnostic"), onClick: click, icon: renderSearchIcon24, loading: loading2, disabled, classNames: ["cbi-button-apply"] }) ]); } // src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts function renderSystemInfo({ items }) { return E("div", { class: "pdk_diagnostic-page__right-bar__system-info" }, [ E( "b", { class: "pdk_diagnostic-page__right-bar__system-info__title" }, _("System information") ), ...items.map((item) => { const tagClass = [ "pdk_diagnostic-page__right-bar__system-info__row__tag", ...insertIf(item.tag?.kind === "neutral", [ "pdk_diagnostic-page__right-bar__system-info__row__tag--neutral" ]), ...insertIf(item.tag?.kind === "warning", [ "pdk_diagnostic-page__right-bar__system-info__row__tag--warning" ]), ...insertIf(item.tag?.kind === "success", [ "pdk_diagnostic-page__right-bar__system-info__row__tag--success" ]) ].filter(Boolean).join(" "); return E( "div", { class: "pdk_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.replace(/^(\d+(?:\.\d+)*)-r(\d+)$/i, "$1-$2"); } // src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts function renderWikiDisclaimer(kind) { const iconWrap = E("span", { class: "pdk_diagnostic-page__right-bar__wiki__icon" }); iconWrap.appendChild(renderBookOpenTextIcon24()); const className = [ "pdk_diagnostic-page__right-bar__wiki", ...insertIf(kind === "error", [ "pdk_diagnostic-page__right-bar__wiki--error" ]), ...insertIf(kind === "warning", [ "pdk_diagnostic-page__right-bar__wiki--warning" ]) ].join(" "); return E("div", { class: className }, [ E("div", { class: "pdk_diagnostic-page__right-bar__wiki__content" }, [ iconWrap, E("div", { class: "pdk_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/ushan0v/podkop-plus#readme", "_blank", "noopener,noreferrer" ) }) ]); } // src/podkop/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 latencyGroup = await PodkopShellMethods.getClashApiGroupLatency( section.code ); const selectedOutbound = section.outbounds.find((item) => item.selected) ?? section.outbounds.find( (item) => item.type?.toLowerCase() === "urltest" ) ?? section.outbounds[0]; const isUrlTest = selectedOutbound?.type?.toLowerCase() === "urltest"; const isSubscription = section.proxyConfigType === "subscription"; const success2 = latencyGroup.success && !latencyGroup.data.message; if (success2) { const latencyValues = Object.values(latencyGroup.data); const sectionState = isSubscription ? getSubscriptionLatencyState(latencyValues) : "success"; if (isUrlTest) { const latency2 = latencyValues.map((item) => item ? `${item}ms` : "n/a").join(" / "); return { state: sectionState, latency: `[${_("Fastest")}] ${latency2}` }; } const selectedProxyDelay = latencyGroup.data?.[selectedOutbound?.code ?? ""]; if (selectedProxyDelay) { return { state: sectionState, latency: `[${selectedOutbound?.displayName ?? ""}] ${selectedProxyDelay}ms` }; } return { state: "error", latency: `[${selectedOutbound?.displayName ?? ""}] ${_("Not responding")}` }; } return { state: "error", latency: _("Not responding") }; } const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency( section.code ); const success = latencyProxy.success && !latencyProxy.data.message; if (success) { return { state: "success", latency: `${latencyProxy.data.delay} ms` }; } 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/podkop/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 servicesInfoRefreshTimer = null; var servicesInfoRefreshPromise = null; function sleep2(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getDiagnosticsProviderOptions(systemInfo = store.get().diagnosticsSystemInfo) { return { includeZapret: Boolean(systemInfo.zapret_installed), includeByedpi: Boolean(systemInfo.byedpi_installed) }; } function getNotRunningDiagnosticsChecks() { return getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions() ); } function resetDiagnosticsChecks() { store.set({ diagnosticsChecks: getNotRunningDiagnosticsChecks() }); } function setDiagnosticActionLoading(action, loading2) { const diagnosticsActions = store.get().diagnosticsActions; store.set({ diagnosticsActions: { ...diagnosticsActions, [action]: { loading: loading2 } } }); } function isDiagnosticMountActive(mountId = diagnosticMountId) { return diagnosticMounted && diagnosticMountId === mountId; } function isMutatingServiceActionLoading() { const actions = store.get().diagnosticsActions; return actions.restart.loading || actions.start.loading || actions.stop.loading || actions.enable.loading || actions.disable.loading; } function getPodkopStatusText(running, enabled) { if (running) { return enabled ? "running & enabled" : "running but disabled"; } return enabled ? "stopped but enabled" : "stopped & disabled"; } function setDisplayedPodkopRunning(running) { const servicesInfoWidget = store.get().servicesInfoWidget; const enabled = Boolean(servicesInfoWidget.data.podkopEnabled); store.set({ servicesInfoWidget: { ...servicesInfoWidget, loading: false, data: { ...servicesInfoWidget.data, podkopRunning: running ? 1 : 0, podkopStatus: getPodkopStatusText(running, enabled) } } }); } async function refreshDiagnosticServicesInfo({ force = false, mountId = diagnosticMountId, allowInactive = false } = {}) { if (!allowInactive && !isDiagnosticMountActive(mountId)) { return; } if (!force && isMutatingServiceActionLoading()) { return; } if (servicesInfoRefreshPromise) { return servicesInfoRefreshPromise; } const promise = fetchServicesInfo().catch((error) => { logger.error( "[DIAGNOSTIC]", "refreshDiagnosticServicesInfo failed", error ); }).finally(() => { if (servicesInfoRefreshPromise === promise) { servicesInfoRefreshPromise = null; } }); servicesInfoRefreshPromise = promise; return promise; } async function waitForPodkopRunningState(expectedRunning) { const startedAt = Date.now(); while (Date.now() - startedAt < SERVICE_ACTION_STATUS_TIMEOUT_MS) { await refreshDiagnosticServicesInfo({ force: true, allowInactive: true }); const podkopRunning = Boolean( store.get().servicesInfoWidget.data.podkopRunning ); if (podkopRunning === expectedRunning) { return true; } await sleep2(SERVICE_STATUS_REFRESH_INTERVAL_MS); } return false; } function startServicesInfoRefreshTimer() { if (servicesInfoRefreshTimer) { clearInterval(servicesInfoRefreshTimer); } servicesInfoRefreshTimer = setInterval(() => { void refreshDiagnosticServicesInfo(); }, SERVICE_STATUS_REFRESH_INTERVAL_MS); } function stopServicesInfoRefreshTimer() { if (!servicesInfoRefreshTimer) { return; } clearInterval(servicesInfoRefreshTimer); servicesInfoRefreshTimer = null; } async function fetchSystemInfo() { const systemInfo = await ensureSystemInfo(); store.set({ diagnosticsChecks: getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions(systemInfo) ) }); } async function fetchDiagnosticsProviderInfo() { const requestId = ++latestProviderInfoRequestId; try { const [zapretRuntime, byedpiRuntime] = await Promise.all([ PodkopShellMethods.checkZapretRuntime(), PodkopShellMethods.checkByedpiRuntime() ]); 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, byedpi_installed: byedpiRuntime.success ? byedpiRuntime.data.byedpi_installed : currentSystemInfo.byedpi_installed }; if (!zapretRuntime.success) { logger.error("[DIAGNOSTIC]", "fetchZapretRuntime failed", zapretRuntime); } if (!byedpiRuntime.success) { logger.error("[DIAGNOSTIC]", "fetchByedpiRuntime failed", byedpiRuntime); } if (!nextSystemInfo.zapret_installed) { nextSystemInfo.zapret_version = "not installed"; } if (!nextSystemInfo.byedpi_installed) { nextSystemInfo.byedpi_version = "not installed"; } store.set({ diagnosticsSystemInfo: nextSystemInfo, diagnosticsChecks: getDiagnosticsChecks( _("Not running"), getDiagnosticsProviderOptions(nextSystemInfo) ) }); } catch (error) { logger.error("[DIAGNOSTIC]", "fetchDiagnosticsProviderInfo failed", error); if (requestId === latestProviderInfoRequestId) { const currentSystemInfo = store.get().diagnosticsSystemInfo; store.set({ diagnosticsSystemInfo: { ...currentSystemInfo, providerInfoLoaded: true } }); } } } async function ensureDiagnosticsProviderInfo() { if (store.get().diagnosticsSystemInfo.providerInfoLoaded) { return; } await fetchDiagnosticsProviderInfo(); } function renderDiagnosticsChecks() { logger.debug("[DIAGNOSTIC]", "renderDiagnosticsChecks"); const diagnosticsChecks = [...store.get().diagnosticsChecks].sort( (a, b) => a.order - b.order ); const container = document.getElementById("pdk_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 container = document.getElementById("pdk_diagnostic-page-run-check"); const renderedAction = renderRunAction({ loading: loading2, disabled: !providerInfoLoaded, click: () => runChecks() }); return preserveScrollForPage(() => { container.replaceChildren(renderedAction); }); } async function handleServiceRuntimeAction({ action, command, expectedRunning, optimisticRunning }) { setDiagnosticActionLoading(action, true); if (optimisticRunning !== void 0) { setDisplayedPodkopRunning(optimisticRunning); } try { await command(); await waitForPodkopRunningState(expectedRunning); } catch (e) { logger.error("[DIAGNOSTIC]", `handleServiceRuntimeAction(${action})`, e); } finally { setDiagnosticActionLoading(action, false); resetDiagnosticsChecks(); } } async function handleRestart() { await handleServiceRuntimeAction({ action: "restart", command: PodkopShellMethods.restart, expectedRunning: true, optimisticRunning: false }); } async function handleStart() { await handleServiceRuntimeAction({ action: "start", command: PodkopShellMethods.start, expectedRunning: true }); } async function handleStop() { await handleServiceRuntimeAction({ action: "stop", command: PodkopShellMethods.stop, expectedRunning: false }); } async function handleEnable() { setDiagnosticActionLoading("enable", true); try { await PodkopShellMethods.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 PodkopShellMethods.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 PodkopShellMethods.globalCheck(); if (globalCheck.success) { ui.showModal( _("Global check"), renderModal(globalCheck.data, "global_check") ); } 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 PodkopShellMethods.checkLogs(); if (viewLogs.success) { const getLatestLogs = async () => { const latestLogs = await PodkopShellMethods.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 PodkopShellMethods.showSingBoxConfig(); if (showSingBoxConfig.success) { ui.showModal( _("Show sing-box config"), renderModal( JSON.stringify(showSingBoxConfig.data, null, 2), "show_sing_box_config" ) ); } 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("pdk_diagnostic-page-wiki"); return preserveScrollForPage(() => { container.replaceChildren(renderWikiDisclaimer(getWikiKind())); }); } function renderDiagnosticAvailableActionsWidget() { const diagnosticsActions = store.get().diagnosticsActions; const servicesInfoWidget = store.get().servicesInfoWidget; logger.debug("[DIAGNOSTIC]", "renderDiagnosticAvailableActionsWidget"); const podkopEnabled = Boolean(servicesInfoWidget.data.podkopEnabled); const podkopRunning = Boolean(servicesInfoWidget.data.podkopRunning); const restartLoading = diagnosticsActions.restart.loading; const atLeastOneMutatingActionLoading = restartLoading || diagnosticsActions.start.loading || diagnosticsActions.stop.loading || diagnosticsActions.enable.loading || diagnosticsActions.disable.loading; const serviceControlsDisabled = servicesInfoWidget.loading || atLeastOneMutatingActionLoading; const utilityActionsDisabled = atLeastOneMutatingActionLoading; const container = document.getElementById("pdk_diagnostic-page-actions"); const renderedActions = renderAvailableActions({ restart: { loading: restartLoading, visible: true, onClick: handleRestart, disabled: serviceControlsDisabled }, start: { loading: diagnosticsActions.start.loading, visible: !podkopRunning, onClick: handleStart, disabled: serviceControlsDisabled }, stop: { loading: diagnosticsActions.stop.loading, visible: podkopRunning, onClick: handleStop, disabled: serviceControlsDisabled }, enable: { loading: diagnosticsActions.enable.loading, visible: !podkopEnabled, onClick: handleEnable, disabled: serviceControlsDisabled }, disable: { loading: diagnosticsActions.disable.loading, visible: podkopEnabled, 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: false }, 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("pdk_diagnostic-page-system-info"); const items = [ { key: "Podkop Plus", value: normalizeCompiledVersion(diagnosticsSystemInfo.podkop_version) }, { key: "Luci App", value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION) }, { key: "Sing-box", value: diagnosticsSystemInfo.sing_box_version } ]; if (diagnosticsSystemInfo.zapret_installed) { items.push({ key: "Zapret", value: diagnosticsSystemInfo.zapret_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) { renderDiagnosticAvailableActionsWidget(); } if (diff.diagnosticsSystemInfo) { renderDiagnosticSystemInfoWidget(); renderDiagnosticRunActionWidget(); } } async function runChecks() { try { await ensureDiagnosticsProviderInfo(); const providerOptions = getDiagnosticsProviderOptions(); const runners = [ runDnsCheck, runSingBoxCheck, runNftCheck, ...providerOptions.includeZapret ? [runZapretCheck] : [], ...providerOptions.includeByedpi ? [runByedpiCheck] : [], runSectionsCheck, runFakeIPCheck ]; store.set({ diagnosticsRunAction: { loading: true }, diagnosticsChecks: getLoadingDiagnosticsChecks(providerOptions).diagnosticsChecks }); for (const runner of runners) { try { await runner(); } catch (e) { logger.error("[DIAGNOSTIC]", `runChecks - ${runner.name} failed`, e); } } } catch (e) { logger.error("[DIAGNOSTIC]", "runChecks - e", e); } finally { store.set({ diagnosticsRunAction: { loading: false } }); } } async function loadInitialDiagnosticData() { const diagnosticStatus = document.getElementById("diagnostic-status"); if (diagnosticStatus?.isConnected && diagnosticStatus.offsetParent !== null) { await fetchSystemInfo(); await ensureDiagnosticsProviderInfo(); } } function onPageMount2() { onPageUnmount2(); diagnosticMounted = true; diagnosticMountId += 1; store.subscribe(onStoreUpdate2); renderDiagnosticsChecks(); renderDiagnosticRunActionWidget(); renderDiagnosticAvailableActionsWidget(); renderDiagnosticSystemInfoWidget(); renderWikiDisclaimerWidget(); void refreshDiagnosticServicesInfo({ force: true }); startServicesInfoRefreshTimer(); void loadInitialDiagnosticData(); } function onPageUnmount2() { diagnosticMounted = false; diagnosticMountId += 1; stopServicesInfoRefreshTimer(); servicesInfoRefreshPromise = null; store.unsubscribe(onStoreUpdate2); store.reset(["diagnosticsRunAction"]); resetDiagnosticsChecks(); } 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"); onPageMount2(); registerLifecycleListeners2(); }); } // src/podkop/tabs/diagnostic/styles.ts var styles4 = ` #cbi-${PODKOP_UCI_PACKAGE}-diagnostic-_mount_node > div { width: 100%; } #cbi-${PODKOP_UCI_PACKAGE}-diagnostic > h3 { display: none; } .pdk_diagnostic-page { display: grid; grid-template-columns: 2fr 1fr; grid-column-gap: 10px; align-items: start; } @media (max-width: 800px) { .pdk_diagnostic-page { grid-template-columns: 1fr; } } .pdk_diagnostic-page__right-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .pdk_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; } .pdk_diagnostic-page__right-bar__wiki--warning { border: 2px var(--warn-color-medium, orange) solid; } .pdk_diagnostic-page__right-bar__wiki--error { border: 2px var(--error-color-medium, red) solid; } .pdk_diagnostic-page__right-bar__wiki__content { display: grid; grid-template-columns: 1fr 5fr; grid-column-gap: 10px; } .pdk_diagnostic-page__right-bar__wiki__texts {} .pdk_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; } .pdk_diagnostic-page__right-bar__actions > .pdk-partial-button { width: 100%; min-width: 0; margin-left: 0; } .pdk_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; } .pdk_diagnostic-page__right-bar__system-info__title { } .pdk_diagnostic-page__right-bar__system-info__row { display: grid; grid-template-columns: auto 1fr; grid-column-gap: 5px; } .pdk_diagnostic-page__right-bar__system-info__row__tag { padding: 2px 4px; border: 1px transparent solid; border-radius: 4px; margin-left: 5px; } .pdk_diagnostic-page__right-bar__system-info__row__tag--neutral { border: 1px var(--background-color-high, gray) solid; color: var(--text-color-medium, gray); } .pdk_diagnostic-page__right-bar__system-info__row__tag--warning { border: 1px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .pdk_diagnostic-page__right-bar__system-info__row__tag--success { border: 1px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .pdk_diagnostic-page__left-bar { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .pdk_diagnostic-page__run_check_wrapper {} .pdk_diagnostic-page__run_check_wrapper button { width: 100%; } .pdk_diagnostic-page__checks { display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .pdk_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; } .pdk_diagnostic_alert--loading { border: 2px var(--primary-color-high, dodgerblue) solid; } .pdk_diagnostic_alert--warning { border: 2px var(--warn-color-medium, orange) solid; color: var(--warn-color-medium, orange); } .pdk_diagnostic_alert--error { border: 2px var(--error-color-medium, red) solid; color: var(--error-color-medium, red); } .pdk_diagnostic_alert--success { border: 2px var(--success-color-medium, green) solid; color: var(--success-color-medium, green); } .pdk_diagnostic_alert--skipped {} .pdk_diagnostic_alert__icon {} .pdk_diagnostic_alert__content {} .pdk_diagnostic_alert__title { display: block; } .pdk_diagnostic_alert__description {} .pdk_diagnostic_alert__summary { margin-top: 10px; } .pdk_diagnostic_alert__summary__item { display: grid; grid-template-columns: 16px auto 1fr; grid-column-gap: 10px; } .pdk_diagnostic_alert__summary__item--error { color: var(--error-color-medium, red); } .pdk_diagnostic_alert__summary__item--warning { color: var(--warn-color-medium, orange); } .pdk_diagnostic_alert__summary__item--success { color: var(--success-color-medium, green); } .pdk_diagnostic_alert__summary__item__icon { width: 16px; height: 16px; } `; // src/podkop/tabs/diagnostic/index.ts var DiagnosticTab = { render: render2, initController: initController2, styles: styles4 }; // src/podkop/tabs/monitoring/render.ts function render3() { return E( "div", { id: "monitoring-status", class: "pdk_monitoring-page" }, [ E("div", { class: "pdk_monitoring-page__panel" }, [ E("div", { class: "pdk_monitoring-page__controls" }, [ E("div", { class: "pdk_monitoring-page__tabs" }, [ E( "button", { id: "monitoring-tab-active", class: "btn cbi-button pdk_monitoring-page__tab pdk_monitoring-page__tab--active", type: "button" }, `${_("Active")} 0` ), E( "button", { id: "monitoring-tab-closed", class: "btn cbi-button pdk_monitoring-page__tab", type: "button" }, `${_("Closed")} 0` ) ]), E("div", { class: "pdk_monitoring-page__filters" }, [ E( "select", { id: "monitoring-device-filter", class: "cbi-input-select pdk_monitoring-page__device-filter" }, [E("option", { value: "all" }, _("All"))] ), E("label", { class: "pdk_monitoring-page__search" }, [ E("span", { class: "pdk_monitoring-page__search-icon" }, []), E("input", { id: "monitoring-search", class: "cbi-input-text pdk_monitoring-page__search-input", type: "search", placeholder: _("Search"), autocomplete: "off" }) ]) ]), E("div", { class: "pdk_monitoring-page__actions" }, [ E( "button", { id: "monitoring-close-all", class: "btn cbi-button pdk_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 pdk_monitoring-page__icon-button", title: _("Pause updates"), "aria-label": _("Pause updates"), type: "button" }, [] ) ]) ]), E( "div", { id: "monitoring-connections", class: "pdk_monitoring-page__body" }, [ E( "div", { class: "pdk_monitoring-page__state pdk_monitoring-page__state--loading" }, _("Loading connections") ) ] ) ]) ] ); } // src/podkop/tabs/monitoring/initController.ts var RENDER_INTERVAL_MS = 500; 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 renderTimer = null; var connectionsSocketUrl = ""; var renderSkippedForSelection = false; var pendingConnectionsPayload = null; 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 activeConnections = /* @__PURE__ */ new Map(); var closedConnections = /* @__PURE__ */ new Map(); var closingConnectionIds = /* @__PURE__ */ new Set(); function normalizeString(value) { return value == null ? "" : String(value).trim(); } 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 = { "direct-out": "bypass" }; const serverMap = {}; const routeSectionItems = []; 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; map[getOutboundTagBySection(`${sectionName}-urltest`)] = 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 getDeviceFilterLabel(ip) { 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("pdk_monitoring-page__tab--active", active); } function renderTabButtonContent(label, count) { return [ E("span", { class: "pdk_monitoring-page__tab-label" }, label), E("span", { class: "pdk_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) ); } if (closedButton) { closedButton.replaceChildren( ...renderTabButtonContent(_("Closed"), closedConnections.size) ); } setButtonActive(activeButton, activeTab === "active"); setButtonActive(closedButton, activeTab === "closed"); if (closeAllButton) { closeAllButton.replaceChildren(renderXIcon24()); closeAllButton.disabled = 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.classList.toggle( "pdk_monitoring-page__icon-button--active", monitoringPaused ); } const searchIcon = document.querySelector( ".pdk_monitoring-page__search-icon" ); if (searchIcon && searchIcon.childNodes.length === 0) { searchIcon.replaceChildren(renderSearchIcon24()); } renderDeviceFilterOptions(); } function renderValue(value, className = "") { const text = value || "-"; const element = E( "span", { class: ["pdk_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: "pdk_monitoring-page__value pdk_monitoring-page__source-value pdk_monitoring-page__source-value--ip-only", title: fullText }, source.primary || "-" ); element2.setAttribute("data-copy-value", fullText); return element2; } const element = E( "span", { class: "pdk_monitoring-page__value pdk_monitoring-page__source-value", title: fullText }, [ E("span", { class: "pdk_monitoring-page__source-name" }, source.primary), E("span", { class: "pdk_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 pdk_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 ? "pdk_monitoring-page__row--closing" : "" }, [ renderTableCell(_("Host"), [renderValue(target.primary)]), renderTableCell(_("Type"), [ renderValue(getNetwork(connection), "pdk_monitoring-page__network") ]), renderTableCell(_("Route"), [ renderValue(getRoute(connection), "pdk_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: "pdk_monitoring-page__state-row" }, [ E( "td", { class: "pdk_monitoring-page__state-cell", colSpan: 8 }, [ E( "div", { class: ["pdk_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: "pdk_monitoring-page__table-wrap" }, [ E( "table", { class: "table cbi-section-table pdk_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 (loading) { container.replaceChildren( renderConnectionsTable([], { text: _("Loading connections"), className: "pdk_monitoring-page__state--loading" }) ); return; } if (failed) { container.replaceChildren( renderConnectionsTable([], { text: _("Connections are unavailable"), className: "pdk_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; } } 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( ".pdk_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 PodkopShellMethods.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 PodkopShellMethods.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); } 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( ".pdk_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 CustomPodkopMethods.getConfigSections()); } catch (error) { logger.warn("[MONITORING]", "loadRouteDisplayNames: failed", error); buildRouteDisplayNames([]); } finally { renderConnections(); } } async function connectToConnectionsSocket() { const mountId = monitoringMountId; const clashApiSecret = await getClashApiSecret2(); if (!monitoringMounted || mountId !== monitoringMountId) { return; } connectionsSocketUrl = `${getClashWsUrl()}/connections?token=${clashApiSecret}`; socket.subscribe( connectionsSocketUrl, (msg) => { try { applyConnectionsPayload(JSON.parse(msg)); } catch (error) { logger.error("[MONITORING]", "connections socket parse failed", error); } }, (_err) => { if (!monitoringMounted || mountId !== monitoringMountId) { return; } failed = true; loading = false; renderConnections(); } ); } function resetMonitoringState() { activeTab = "active"; selectedDeviceFilter = ALL_FILTER_VALUE; searchQuery = ""; lastDeviceFilterSignature = ""; loading = true; failed = false; closingAll = false; monitoringPaused = false; monitoringPausedAt = null; pendingConnectionsPayload = null; activeConnections.clear(); closedConnections.clear(); closingConnectionIds.clear(); const searchInput = document.getElementById( "monitoring-search" ); if (searchInput) { searchInput.value = ""; } } function onPageMount3() { onPageUnmount3(); monitoringMounted = true; monitoringMountId += 1; resetMonitoringState(); bindControls(); renderControls(); renderConnections(); void loadLocalDevices(); void loadRouteDisplayNames(); void connectToConnectionsSocket(); 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; } if (connectionsSocketUrl) { socket.disconnect(connectionsSocketUrl); connectionsSocketUrl = ""; } 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/podkop/tabs/monitoring/styles.ts var styles5 = ` #cbi-${PODKOP_UCI_PACKAGE}-monitoring-_mount_node { margin: 16px 0 22px; padding: 0; } #cbi-${PODKOP_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-title { display: none; } #cbi-${PODKOP_UCI_PACKAGE}-monitoring-_mount_node > .cbi-value-field { margin-left: 0; width: 100%; } #cbi-${PODKOP_UCI_PACKAGE}-monitoring-_mount_node > div { width: 100%; } #cbi-${PODKOP_UCI_PACKAGE}-monitoring > h3 { display: none; } .pdk_monitoring-page { --pdk-monitoring-control-height: 34px; --pdk-monitoring-row-action-size: 24px; --pdk-monitoring-divider-color: rgba(127, 127, 127, 0.22); --pdk-monitoring-soft-bg: rgba(127, 127, 127, 0.08); --pdk-monitoring-soft-bg-hover: rgba(127, 127, 127, 0.14); --pdk-monitoring-danger-color: var(--error-color-medium, #d32f2f); --pdk-monitoring-success-color: var(--success-color-medium, #2e7d32); --pdk-monitoring-paused-color: var(--primary-color-high, #1976d2); width: 100%; min-width: 0; } .pdk_monitoring-page__panel { margin-top: 0; border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; box-sizing: border-box; width: 100%; min-width: 0; } .pdk_monitoring-page .btn.pdk_monitoring-page__icon-button { width: var(--pdk-monitoring-control-height); height: var(--pdk-monitoring-control-height); min-width: var(--pdk-monitoring-control-height); min-height: var(--pdk-monitoring-control-height); 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(--pdk-monitoring-divider-color) !important; border-radius: 4px; background: var(--pdk-monitoring-soft-bg) !important; color: var(--text-color-medium) !important; box-shadow: none; } .pdk_monitoring-page .btn.pdk_monitoring-page__icon-button:hover:not(:disabled) { background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .pdk_monitoring-page .btn.pdk_monitoring-page__icon-button--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; } .pdk_monitoring-page .btn.pdk_monitoring-page__icon-button:disabled { opacity: 0.45; cursor: not-allowed; } .pdk_monitoring-page #monitoring-close-all.btn.pdk_monitoring-page__icon-button { border-color: var(--pdk-monitoring-danger-color) !important; background: var(--pdk-monitoring-soft-bg) !important; color: var(--pdk-monitoring-danger-color) !important; } .pdk_monitoring-page #monitoring-close-all.btn.pdk_monitoring-page__icon-button:hover:not(:disabled) { border-color: var(--pdk-monitoring-danger-color) !important; background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--pdk-monitoring-danger-color) !important; } .pdk_monitoring-page #monitoring-pause-toggle.btn.pdk_monitoring-page__icon-button { border-color: var(--pdk-monitoring-success-color) !important; background: var(--pdk-monitoring-soft-bg) !important; color: var(--pdk-monitoring-success-color) !important; } .pdk_monitoring-page #monitoring-pause-toggle.btn.pdk_monitoring-page__icon-button:hover:not(:disabled) { border-color: var(--pdk-monitoring-success-color) !important; background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--pdk-monitoring-success-color) !important; } .pdk_monitoring-page #monitoring-pause-toggle.btn.pdk_monitoring-page__icon-button--active { border-color: var(--pdk-monitoring-paused-color) !important; background: var(--pdk-monitoring-soft-bg) !important; color: var(--pdk-monitoring-paused-color) !important; } .pdk_monitoring-page #monitoring-pause-toggle.btn.pdk_monitoring-page__icon-button--active:hover:not(:disabled) { border-color: var(--pdk-monitoring-paused-color) !important; background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--pdk-monitoring-paused-color) !important; } .pdk_monitoring-page__icon-button svg, .pdk_monitoring-page__row-action svg { width: 16px; height: 16px; display: block; flex: 0 0 auto; } .pdk_monitoring-page__controls { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; justify-content: stretch; gap: 10px; width: 100%; min-width: 0; } .pdk_monitoring-page__actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; } .pdk_monitoring-page__tabs { display: inline-flex; align-items: center; gap: 2px; width: max-content; padding: 2px; border: 1px solid var(--pdk-monitoring-divider-color); border-radius: 6px; background: var(--pdk-monitoring-soft-bg); box-sizing: border-box; } .pdk_monitoring-page .btn.pdk_monitoring-page__tab { height: calc(var(--pdk-monitoring-control-height) - 6px); min-height: calc(var(--pdk-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; } .pdk_monitoring-page .btn.pdk_monitoring-page__tab:hover { background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--text-color-high) !important; } .pdk_monitoring-page .btn.pdk_monitoring-page__tab--active { background: rgba(25, 118, 210, 0.16) !important; color: var(--primary-color-high, #1976d2) !important; font-weight: 700; } .pdk_monitoring-page__tab-label { display: inline-block; } .pdk_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; } .pdk_monitoring-page__tab--active .pdk_monitoring-page__tab-badge { background: rgba(25, 118, 210, 0.22); color: var(--primary-color-high, #1976d2); } .pdk_monitoring-page__filters { display: grid; grid-template-columns: minmax(150px, 220px) minmax(200px, 320px); align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; } .pdk_monitoring-page__device-filter { width: 100%; min-width: 0; height: var(--pdk-monitoring-control-height) !important; min-height: var(--pdk-monitoring-control-height) !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; box-sizing: border-box; line-height: calc(var(--pdk-monitoring-control-height) - 2px) !important; } .pdk_monitoring-page__search { position: relative; display: flex; align-items: center; width: 100%; min-width: 0; height: var(--pdk-monitoring-control-height); margin: 0; } .pdk_monitoring-page__search-icon { position: absolute; left: 8px; width: 16px; height: 16px; color: var(--text-color-medium); pointer-events: none; } .pdk_monitoring-page__search-icon svg { width: 16px; height: 16px; display: block; } .pdk_monitoring-page__search-input { width: 100%; height: var(--pdk-monitoring-control-height) !important; min-height: var(--pdk-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(--pdk-monitoring-control-height) - 2px) !important; } .pdk_monitoring-page__body { margin-top: 10px; width: 100%; min-width: 0; } .pdk_monitoring-page__table-wrap { width: 100%; overflow-x: auto; margin-bottom: 0; } .pdk_monitoring-page__table { width: 100%; min-width: 840px; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 0; } .pdk_monitoring-page__table th, .pdk_monitoring-page__table td { padding: 8px 6px; border-bottom: 1px solid var(--pdk-monitoring-divider-color); box-sizing: border-box; text-align: center; vertical-align: middle; overflow: hidden; white-space: nowrap; } .pdk_monitoring-page__table th { color: var(--text-color-medium); font-weight: 700; white-space: nowrap; border-bottom-color: rgba(127, 127, 127, 0.32); } .pdk_monitoring-page__table th:nth-child(1) { width: 28%; } .pdk_monitoring-page__table th:nth-child(2) { width: 6%; } .pdk_monitoring-page__table th:nth-child(3) { width: 16%; } .pdk_monitoring-page__table th:nth-child(4) { width: 8%; } .pdk_monitoring-page__table th:nth-child(5) { width: 9.5%; } .pdk_monitoring-page__table th:nth-child(6) { width: 8.5%; } .pdk_monitoring-page__table th:nth-child(7) { width: 16%; } .pdk_monitoring-page__table th:nth-child(8) { width: 8%; } .pdk_monitoring-page__table tbody tr:last-child td { border-bottom: 0; } .pdk_monitoring-page__table td:last-child { padding-top: 0; padding-bottom: 0; } .pdk_monitoring-page__value { display: block; max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center; line-height: 1.3; color: var(--text-color-high); user-select: text; } .pdk_monitoring-page__source-value { display: flex; align-items: baseline; justify-content: center; gap: 5px; } .pdk_monitoring-page__source-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pdk_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; } .pdk_monitoring-page__source-value--ip-only { color: var(--text-color-high); } .pdk_monitoring-page__cell-main { color: var(--text-color-high); font-weight: 600; line-height: 1.25; } .pdk_monitoring-page__cell-secondary { margin-top: 2px; color: var(--text-color-medium); font-size: 12px; line-height: 1.25; } .pdk_monitoring-page__route { font-weight: 600; } .pdk_monitoring-page__network { text-transform: lowercase; } .pdk_monitoring-page .btn.pdk_monitoring-page__row-action { width: var(--pdk-monitoring-row-action-size); height: var(--pdk-monitoring-row-action-size); min-width: var(--pdk-monitoring-row-action-size); min-height: var(--pdk-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(--pdk-monitoring-danger-color) !important; box-shadow: none; cursor: pointer; } .pdk_monitoring-page__row-action svg { width: 14px; height: 14px; } .pdk_monitoring-page .btn.pdk_monitoring-page__row-action:hover:not(:disabled) { background: var(--pdk-monitoring-soft-bg-hover) !important; color: var(--pdk-monitoring-danger-color) !important; } .pdk_monitoring-page .btn.pdk_monitoring-page__row-action:disabled { opacity: 0.45; cursor: wait; } .pdk_monitoring-page__row--closing { opacity: 0.55; } .pdk_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; } .pdk_monitoring-page__state-cell { padding: 0 !important; } .pdk_monitoring-page__state--error { color: var(--error-color-medium, #d32f2f); } @media (max-width: 900px) { .pdk_monitoring-page__controls { grid-template-columns: 1fr auto; } .pdk_monitoring-page__tabs { grid-column: 1 / -1; } .pdk_monitoring-page__filters { grid-template-columns: minmax(150px, 220px) minmax(180px, 320px); justify-content: stretch; } .pdk_monitoring-page__device-filter, .pdk_monitoring-page__search { max-width: none; } .pdk_monitoring-page__table { min-width: 0; } .pdk_monitoring-page__table thead { display: none; } .pdk_monitoring-page__table, .pdk_monitoring-page__table tbody, .pdk_monitoring-page__table tr, .pdk_monitoring-page__table td { display: block; width: 100%; } .pdk_monitoring-page__table tr { border: 1px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 8px; box-sizing: border-box; margin-bottom: 8px; } .pdk_monitoring-page__table td { display: grid; grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); gap: 8px; border: 0; border-bottom: 1px solid var(--pdk-monitoring-divider-color); padding: 4px 0; box-sizing: border-box; text-align: left; } .pdk_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; } .pdk_monitoring-page__table td:last-child { grid-template-columns: minmax(92px, 34%) minmax(0, 1fr); align-items: center; border-bottom: 0; min-height: var(--pdk-monitoring-row-action-size); padding: 0; } .pdk_monitoring-page__value { text-align: right; } .pdk_monitoring-page__source-value { justify-content: flex-end; } .pdk_monitoring-page__state-row td::before { display: none; } } @media (max-width: 520px) { .pdk_monitoring-page__controls, .pdk_monitoring-page__filters { align-items: stretch; } .pdk_monitoring-page__tabs, .pdk_monitoring-page__filters, .pdk_monitoring-page__device-filter, .pdk_monitoring-page__search { width: 100%; } .pdk_monitoring-page__controls, .pdk_monitoring-page__filters { grid-template-columns: 1fr; } .pdk_monitoring-page__tabs { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); width: 100%; } .pdk_monitoring-page__table td { grid-template-columns: 1fr; gap: 2px; } .pdk_monitoring-page__value { text-align: left; } .pdk_monitoring-page__source-value { justify-content: flex-start; } } `; // src/podkop/tabs/monitoring/index.ts var MonitoringTab = { render: render3, initController: initController3, styles: styles5 }; // src/podkop/tabs/updates/render.ts function render4() { return E("div", { id: "updates-status", class: "pdk_updates-page" }, [ E("div", { id: "pdk_updates-components", class: "pdk_updates-page__components" }) ]); } // src/podkop/tabs/updates/initController.ts var updatesLifecycleRegistered = false; var updatesControllerInitialized = false; var updatesMounted = false; function isNotInstalled(version) { return !version || version === "not installed"; } function getCheckTag(component) { const status = store.get().updatesChecks[component].status; if (!status) { return void 0; } if (status === "latest") { return { label: _("Latest"), kind: "success" }; } if (status === "outdated") { return { label: _("Outdated"), kind: "warning" }; } return { label: _("Dev"), kind: "neutral" }; } function shouldShowInstallAfterCheck(component) { const status = store.get().updatesChecks[component].status; return status === "outdated" || status === "dev"; } function getInstallActionText(component) { const checkResult = store.get().updatesChecks[component]; if (shouldShowInstallAfterCheck(component) && checkResult.latest_version) { return _("Install %s").replace("%s", checkResult.latest_version); } return _("Install"); } function getGitHubReleaseUrl(component) { const checkResult = store.get().updatesChecks[component]; if (!shouldShowInstallAfterCheck(component) || !checkResult.release_url) { return void 0; } return checkResult.release_url; } function isAnyActionLoading() { return Object.values(store.get().updatesActions).some((item) => item.loading); } function isSystemInfoLoading() { const systemInfo = store.get().diagnosticsSystemInfo; return systemInfo.loading || !systemInfo.loaded; } function setActionLoading(action, loading2) { const updatesActions = store.get().updatesActions; store.set({ updatesActions: { ...updatesActions, [action]: { loading: loading2 } } }); } 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 getExpectedLatestVersionForAction(button) { if (button.component !== "podkop" || 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(PODKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT, { detail: { zapretInstalled: Boolean(systemInfo.zapret_installed), byedpiInstalled: Boolean(systemInfo.byedpi_installed) } }) ); } function reloadPageAfterPodkopUpdate() { 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 === "podkop" && result.action === "install") { nextSystemInfo.podkop_version = version; } if (result.component === "sing_box") { nextSystemInfo.sing_box_version = version; if (result.action === "install_extended") { nextSystemInfo.sing_box_extended = 1; } if (result.action === "install_stable") { nextSystemInfo.sing_box_extended = 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 === "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; } } store.set({ diagnosticsSystemInfo: nextSystemInfo }); if (result.component === "zapret" || result.component === "byedpi") { notifyActionProvidersAvailabilityChanged(nextSystemInfo); } } async function handleComponentAction(button) { if (isAnyActionLoading()) { return; } setActionLoading(button.key, true); try { const response = await PodkopShellMethods.componentAction( button.component, button.action, getExpectedLatestVersionForAction(button) ); if (!response.success) { setActionLoading(button.key, false); showToast(response.error || _("Failed to execute"), "error"); return; } const result = response.data; if (button.action === "check_update") { const status = result.status || null; if (status === "latest" || status === "outdated" || status === "dev") { setCheckResult( button.component, status, result.latest_version || "", result.release_url || "" ); } setActionLoading(button.key, false); showToast(getCheckToastMessage(status), "success"); return; } if (button.action === "install" || button.action.startsWith("install_")) { setCheckResult(button.component, "latest", result.latest_version || ""); } else { resetCheckResult(button.component); } patchSystemInfoAfterMutation(result); setActionLoading(button.key, false); if (result.component === "podkop" && result.action === "install") { if (result.message) { showToast(result.message, "success", 1200); } reloadPageAfterPodkopUpdate(); return; } if (result.message) { showToast(result.message, "success"); } void refreshSystemInfoAfterMutation(); } catch (error) { logger.error("[UPDATES]", "handleComponentAction failed", error); setActionLoading(button.key, false); showToast(_("Failed to execute"), "error"); } } function getPrimaryUpdateAction(component, checkKey, installKey) { if (shouldShowInstallAfterCheck(component)) { return { key: installKey, text: getInstallActionText(component), icon: renderRotateCcwIcon24, component, action: "install" }; } return { key: checkKey, text: _("Check update"), icon: renderSearchIcon24, component, action: "check_update" }; } function getComponentCards() { const systemInfo = store.get().diagnosticsSystemInfo; const systemInfoLoading = isSystemInfoLoading(); const zapretInstalled = Boolean(systemInfo.zapret_installed); const byedpiInstalled = Boolean(systemInfo.byedpi_installed); const singBoxInstallAction = systemInfo.sing_box_extended ? { key: "singBoxInstallStable", text: _("Install stable"), icon: renderRotateCcwIcon24, component: "sing_box", action: "install_stable" } : { key: "singBoxInstallExtended", text: _("Install extended"), icon: renderRotateCcwIcon24, component: "sing_box", action: "install_extended" }; return [ { title: "Podkop Plus", version: normalizeCompiledVersion(systemInfo.podkop_version), releaseUrl: getGitHubReleaseUrl("podkop"), tag: getCheckTag("podkop"), actions: [ getPrimaryUpdateAction("podkop", "podkopCheck", "podkopInstall") ] }, { title: "Sing-box", version: isNotInstalled(systemInfo.sing_box_version) ? _("Not installed") : systemInfo.sing_box_version, releaseUrl: getGitHubReleaseUrl("sing_box"), tag: getCheckTag("sing_box"), actions: [ getPrimaryUpdateAction( "sing_box", "singBoxCheck", "singBoxInstall" ), singBoxInstallAction ] }, { title: "Zapret", version: systemInfoLoading ? "loading" : zapretInstalled ? systemInfo.zapret_version : _("Not installed"), releaseUrl: getGitHubReleaseUrl("zapret"), tag: zapretInstalled ? getCheckTag("zapret") : void 0, actions: zapretInstalled ? [ getPrimaryUpdateAction("zapret", "zapretCheck", "zapretInstall"), { key: "zapretRemove", text: _("Remove"), icon: renderXIcon24, component: "zapret", action: "remove" } ] : [ { key: "zapretInstall", text: _("Install"), icon: renderRotateCcwIcon24, component: "zapret", action: "install" } ] }, { title: "ByeDPI", version: systemInfoLoading ? "loading" : byedpiInstalled ? systemInfo.byedpi_version : _("Not installed"), releaseUrl: getGitHubReleaseUrl("byedpi"), tag: byedpiInstalled ? getCheckTag("byedpi") : void 0, actions: byedpiInstalled ? [ getPrimaryUpdateAction("byedpi", "byedpiCheck", "byedpiInstall"), { key: "byedpiRemove", text: _("Remove"), icon: renderXIcon24, component: "byedpi", action: "remove" } ] : [ { key: "byedpiInstall", text: _("Install"), icon: renderRotateCcwIcon24, component: "byedpi", action: "install" } ] } ]; } function renderComponentTag(card) { if (!card.tag) { return null; } return E( "span", { class: [ "pdk_updates-page__component__tag", card.tag.kind === "success" ? "pdk_updates-page__component__tag--success" : "", card.tag.kind === "warning" ? "pdk_updates-page__component__tag--warning" : "" ].filter(Boolean).join(" ") }, card.tag.label ); } function renderComponentCard(card) { const updatesActions = store.get().updatesActions; const anyActionLoading = isAnyActionLoading(); const systemInfoLoading = isSystemInfoLoading(); const tag = renderComponentTag(card); const headerChildren = [ E("b", { class: "pdk_updates-page__component__title" }, card.title) ]; const statusChildren = []; if (card.releaseUrl) { statusChildren.push( E( "a", { class: "pdk_updates-page__component__release-link", href: card.releaseUrl, target: "_blank", rel: "noopener noreferrer" }, _("GitHub release") ) ); } if (tag) { statusChildren.push(tag); } if (statusChildren.length > 0) { headerChildren.push( E( "div", { class: "pdk_updates-page__component__status" }, statusChildren ) ); } return E("div", { class: "pdk_updates-page__component" }, [ E( "div", { class: "pdk_updates-page__component__header" }, headerChildren ), E("div", { class: "pdk_updates-page__component__version" }, [ E( "span", { class: "pdk_updates-page__component__version__label" }, _("Version") ), E( "span", { class: "pdk_updates-page__component__version__value" }, card.version ) ]), E( "div", { class: "pdk_updates-page__component__actions" }, card.actions.map((action) => { const loading2 = updatesActions[action.key].loading; return renderButton({ text: action.text, icon: action.icon, loading: loading2, disabled: systemInfoLoading || anyActionLoading && !loading2, onClick: () => void handleComponentAction(action) }); }) ) ]); } function renderUpdatesComponents() { const container = document.getElementById("pdk_updates-components"); if (!container) { return; } const renderedComponents = getComponentCards().map(renderComponentCard); return preserveScrollForPage(() => { container.replaceChildren(...renderedComponents); }); } function onStoreUpdate3(_next, _prev, diff) { if (diff.diagnosticsSystemInfo || diff.updatesActions || diff.updatesChecks) { renderUpdatesComponents(); } } function onPageMount4() { onPageUnmount4(); updatesMounted = true; store.subscribe(onStoreUpdate3); renderUpdatesComponents(); void ensureSystemInfo(); } function onPageUnmount4() { updatesMounted = false; 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; onMount("updates-status").then(() => { logger.debug("[UPDATES]", "initController", "onMount"); onPageMount4(); registerLifecycleListeners4(); }); } // src/podkop/tabs/updates/styles.ts var styles6 = ` #cbi-${PODKOP_UCI_PACKAGE}-updates-_mount_node > div { width: 100%; } #cbi-${PODKOP_UCI_PACKAGE}-updates > h3 { display: none; } .pdk_updates-page { width: 100%; } .pdk_updates-page__components { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); grid-gap: 10px; } @media (max-width: 760px) { .pdk_updates-page__components { grid-template-columns: 1fr; } } .pdk_updates-page__component { border: 2px var(--background-color-low, lightgray) solid; border-radius: 4px; padding: 10px; min-width: 0; display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; } .pdk_updates-page__component__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; min-width: 0; } .pdk_updates-page__component__title { color: var(--text-color-high); line-height: 1.25; overflow-wrap: anywhere; } .pdk_updates-page__component__status { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; flex: 0 0 auto; } .pdk_updates-page__component__version { display: grid; grid-template-columns: auto 1fr; grid-column-gap: 6px; align-items: baseline; min-width: 0; } .pdk_updates-page__component__version__label { color: var(--text-color-medium); } .pdk_updates-page__component__version__value { min-width: 0; overflow-wrap: anywhere; } .pdk_updates-page__component__tag { flex: 0 0 auto; padding: 2px 5px; border: 1px var(--background-color-high, gray) solid; border-radius: 4px; color: var(--text-color-medium, gray); line-height: 1.2; } .pdk_updates-page__component__tag--success { border-color: var(--success-color-medium, green); color: var(--success-color-medium, green); } .pdk_updates-page__component__tag--warning { border-color: var(--warn-color-medium, orange); color: var(--warn-color-medium, orange); } .pdk_updates-page__component__actions { display: flex; flex-wrap: wrap; gap: 6px; } .pdk_updates-page__component__actions > .pdk-partial-button { margin-left: 0; } .pdk_updates-page__component__release-link { font-size: 11px; line-height: 1.2; text-align: right; white-space: nowrap; } `; // src/podkop/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-${PODKOP_UCI_PACKAGE}-settings > h3 { display: none; } /* Hide extra H3 for rules tab */ #cbi-${PODKOP_UCI_PACKAGE}-section > h3:nth-child(1) { display: none; } /* Vertical align for remove rule action button */ #cbi-${PODKOP_UCI_PACKAGE}-section > .cbi-section-remove { margin-bottom: -32px; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-actions > div { display: inline-flex; align-items: center; gap: 4px; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-actions { text-align: right; } /* Rule reorder visuals */ #cbi-${PODKOP_UCI_PACKAGE}-section { position: relative; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-table-row { position: relative; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-table-row.placeholder { opacity: 1; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-table-row.placeholder em { font-style: italic; } #cbi-${PODKOP_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after, #cbi-${PODKOP_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-${PODKOP_UCI_PACKAGE}-section .cbi-section-table-row.drag-over-above::after { top: -1px; } #cbi-${PODKOP_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 PODKOP_GLOBAL_STYLES_ID = "podkop-global-styles"; function injectGlobalStyles() { if (document.getElementById(PODKOP_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, DNS_SERVER_OPTIONS, DOMAIN_LIST_OPTIONS, DashboardTab, DiagnosticTab, MonitoringTab, PODKOP_ACTION_PROVIDERS_AVAILABILITY_EVENT, PODKOP_UCI_PACKAGE, PodkopShellMethods, REGIONAL_OPTIONS, UpdatesTab, bulkValidate, coreService, getClashUIUrl, injectGlobalStyles, parseValueList, showToast, store, validateDNS, validateDomain, validateIPV4, validateOutboundJson, validatePath, validateProxyUrl, validateSubnet, validateUrl });