9306 lines
261 KiB
JavaScript
9306 lines
261 KiB
JavaScript
// 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",
|
|
`
|
|
<style id="${PODKOP_GLOBAL_STYLES_ID}">
|
|
${GlobalStyles}
|
|
</style>
|
|
`
|
|
);
|
|
}
|
|
|
|
// 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
|
|
});
|