mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-06 11:36:50 +03:00
1962 lines
51 KiB
JavaScript
1962 lines
51 KiB
JavaScript
// This file is autogenerated, please don't change manually
|
|
"use strict";
|
|
"require baseclass";
|
|
"require fs";
|
|
"require uci";
|
|
|
|
// 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") };
|
|
}
|
|
if (validateIPV4(value).valid) {
|
|
return { valid: true, message: _("Valid") };
|
|
}
|
|
if (validateDomain(value).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:"]) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!protocols.includes(parsedUrl.protocol)) {
|
|
return {
|
|
valid: false,
|
|
message: `${_("URL must use one of the following protocols:")} ${protocols.join(", ")}`
|
|
};
|
|
}
|
|
return { valid: true, message: _("Valid") };
|
|
} catch (_e) {
|
|
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") {
|
|
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/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/validators/validateVlessUrl.ts
|
|
function validateVlessUrl(url) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!url || /\s/.test(url)) {
|
|
return {
|
|
valid: false,
|
|
message: _("Invalid VLESS URL: must not contain spaces")
|
|
};
|
|
}
|
|
if (parsedUrl.protocol !== "vless:") {
|
|
return {
|
|
valid: false,
|
|
message: _("Invalid VLESS URL: must start with vless://")
|
|
};
|
|
}
|
|
if (!parsedUrl.username) {
|
|
return { valid: false, message: _("Invalid VLESS URL: missing UUID") };
|
|
}
|
|
if (!parsedUrl.hostname) {
|
|
return { valid: false, message: _("Invalid VLESS URL: missing server") };
|
|
}
|
|
if (!parsedUrl.port) {
|
|
return { valid: false, message: _("Invalid VLESS URL: missing port") };
|
|
}
|
|
if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
|
|
)
|
|
};
|
|
}
|
|
if (!parsedUrl.search) {
|
|
return {
|
|
valid: false,
|
|
message: _("Invalid VLESS URL: missing query parameters")
|
|
};
|
|
}
|
|
const params = new URLSearchParams(parsedUrl.search);
|
|
const type = params.get("type");
|
|
const validTypes = [
|
|
"tcp",
|
|
"raw",
|
|
"udp",
|
|
"grpc",
|
|
"http",
|
|
"httpupgrade",
|
|
"xhttp",
|
|
"ws",
|
|
"kcp"
|
|
];
|
|
if (!type || !validTypes.includes(type)) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
|
|
)
|
|
};
|
|
}
|
|
const security = params.get("security");
|
|
const validSecurities = ["tls", "reality", "none"];
|
|
if (!security || !validSecurities.includes(security)) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid VLESS URL: security must be one of tls, reality, none"
|
|
)
|
|
};
|
|
}
|
|
if (security === "reality") {
|
|
if (!params.get("pbk")) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid VLESS URL: missing pbk parameter for reality security"
|
|
)
|
|
};
|
|
}
|
|
if (!params.get("fp")) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid VLESS URL: missing fp parameter for reality security"
|
|
)
|
|
};
|
|
}
|
|
}
|
|
return { valid: true, message: _("Valid") };
|
|
} catch (_e) {
|
|
return { valid: false, message: _("Invalid VLESS URL: parsing failed") };
|
|
}
|
|
}
|
|
|
|
// src/validators/validateOutboundJson.ts
|
|
function validateOutboundJson(value) {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (!parsed.type || !parsed.server || !parsed.server_port) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
'Outbound JSON must contain at least "type", "server" and "server_port" fields'
|
|
)
|
|
};
|
|
}
|
|
return { valid: true, message: _("Valid") };
|
|
} catch {
|
|
return { valid: false, message: _("Invalid JSON format") };
|
|
}
|
|
}
|
|
|
|
// src/validators/validateTrojanUrl.ts
|
|
function validateTrojanUrl(url) {
|
|
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")
|
|
};
|
|
}
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
|
return {
|
|
valid: false,
|
|
message: _(
|
|
"Invalid Trojan URL: must contain username, hostname and port"
|
|
)
|
|
};
|
|
}
|
|
} catch (_e) {
|
|
return { valid: false, message: _("Invalid Trojan URL: parsing failed") };
|
|
}
|
|
return { valid: true, message: _("Valid") };
|
|
}
|
|
|
|
// src/validators/validateProxyUrl.ts
|
|
function validateProxyUrl(url) {
|
|
if (url.startsWith("ss://")) {
|
|
return validateShadowsocksUrl(url);
|
|
}
|
|
if (url.startsWith("vless://")) {
|
|
return validateVlessUrl(url);
|
|
}
|
|
if (url.startsWith("trojan://")) {
|
|
return validateTrojanUrl(url);
|
|
}
|
|
return {
|
|
valid: false,
|
|
message: _("URL must start with vless:// or ss:// or trojan://")
|
|
};
|
|
}
|
|
|
|
// src/helpers/getBaseUrl.ts
|
|
function getBaseUrl() {
|
|
const { protocol, hostname } = window.location;
|
|
return `${protocol}//${hostname}`;
|
|
}
|
|
|
|
// src/helpers/parseValueList.ts
|
|
function parseValueList(value) {
|
|
return value.split(/\n/).map((line) => line.split("//")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
// src/styles.ts
|
|
var GlobalStyles = `
|
|
.cbi-value {
|
|
margin-bottom: 10px !important;
|
|
}
|
|
|
|
#diagnostics-status .table > div {
|
|
background: var(--background-color-primary);
|
|
border: 1px solid var(--border-color-medium);
|
|
border-radius: var(--border-radius);
|
|
}
|
|
|
|
#diagnostics-status .table > div pre,
|
|
#diagnostics-status .table > div div[style*="monospace"] {
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
#diagnostics-status .alert-message {
|
|
background: var(--background-color-primary);
|
|
border-color: var(--border-color-medium);
|
|
}
|
|
|
|
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
|
|
display: none;
|
|
}
|
|
|
|
#cbi-podkop-main-_status > div {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Dashboard styles */
|
|
|
|
.pdk_dashboard-page {
|
|
width: 100%;
|
|
--dashboard-grid-columns: 4;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.pdk_dashboard-page {
|
|
--dashboard-grid-columns: 2;
|
|
}
|
|
}
|
|
|
|
.pdk_dashboard-page__widgets-section {
|
|
margin-top: 10px;
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
|
grid-gap: 10px;
|
|
}
|
|
|
|
.pdk_dashboard-page__widgets-section__item {
|
|
border: 2px var(--background-color-low, lightgray) solid;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.pdk_dashboard-page__outbound-section__title-section__title {
|
|
color: var(--text-color-high);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pdk_dashboard-page__outbound-grid {
|
|
margin-top: 5px;
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
|
grid-gap: 10px;
|
|
}
|
|
|
|
.pdk_dashboard-page__outbound-grid__item {
|
|
border: 2px var(--background-color-low, lightgray) solid;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
transition: border 0.2s ease;
|
|
}
|
|
|
|
.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__footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.pdk_dashboard-page__outbound-grid__item__type {}
|
|
|
|
.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);
|
|
}
|
|
|
|
.centered {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* 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%;
|
|
}
|
|
}
|
|
`;
|
|
|
|
// src/helpers/injectGlobalStyles.ts
|
|
function injectGlobalStyles() {
|
|
document.head.insertAdjacentHTML(
|
|
"beforeend",
|
|
`
|
|
<style>
|
|
${GlobalStyles}
|
|
</style>
|
|
`
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
|
|
}
|
|
}
|
|
|
|
// src/constants.ts
|
|
var STATUS_COLORS = {
|
|
SUCCESS: "#4caf50",
|
|
ERROR: "#f44336",
|
|
WARNING: "#ff9800"
|
|
};
|
|
var PODKOP_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__";
|
|
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",
|
|
"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",
|
|
hetzner: "Hetzner ASN",
|
|
ovh: "OVH ASN",
|
|
digitalocean: "Digital Ocean ASN",
|
|
cloudfront: "CloudFront ASN"
|
|
};
|
|
var UPDATE_INTERVAL_OPTIONS = {
|
|
"1h": "Every hour",
|
|
"3h": "Every 3 hours",
|
|
"12h": "Every 12 hours",
|
|
"1d": "Every day",
|
|
"3d": "Every 3 days"
|
|
};
|
|
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 DIAGNOSTICS_UPDATE_INTERVAL = 1e4;
|
|
var CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1e3;
|
|
var ERROR_POLL_INTERVAL = 1e4;
|
|
var COMMAND_TIMEOUT = 1e4;
|
|
var FETCH_TIMEOUT = 1e4;
|
|
var BUTTON_FEEDBACK_TIMEOUT = 1e3;
|
|
var DIAGNOSTICS_INITIAL_DELAY = 100;
|
|
var COMMAND_SCHEDULING = {
|
|
P0_PRIORITY: 0,
|
|
// Highest priority (no delay)
|
|
P1_PRIORITY: 100,
|
|
// Very high priority
|
|
P2_PRIORITY: 300,
|
|
// High priority
|
|
P3_PRIORITY: 500,
|
|
// Above average
|
|
P4_PRIORITY: 700,
|
|
// Standard priority
|
|
P5_PRIORITY: 900,
|
|
// Below average
|
|
P6_PRIORITY: 1100,
|
|
// Low priority
|
|
P7_PRIORITY: 1300,
|
|
// Very low priority
|
|
P8_PRIORITY: 1500,
|
|
// Background execution
|
|
P9_PRIORITY: 1700,
|
|
// Idle mode execution
|
|
P10_PRIORITY: 1900
|
|
// Lowest priority
|
|
};
|
|
|
|
// src/helpers/executeShellCommand.ts
|
|
async function executeShellCommand({
|
|
command,
|
|
args,
|
|
timeout = COMMAND_TIMEOUT
|
|
}) {
|
|
try {
|
|
return withTimeout(
|
|
fs.exec(command, args),
|
|
timeout,
|
|
[command, ...args].join(" ")
|
|
);
|
|
} catch (err) {
|
|
const error = err;
|
|
return { stdout: "", stderr: error?.message, code: 0 };
|
|
}
|
|
}
|
|
|
|
// src/helpers/maskIP.ts
|
|
function maskIP(ip = "") {
|
|
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
|
|
}
|
|
|
|
// 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
|
|
async function onMount(id) {
|
|
return new Promise((resolve) => {
|
|
const el = document.getElementById(id);
|
|
if (el && el.offsetParent !== null) {
|
|
return resolve(el);
|
|
}
|
|
const observer = new MutationObserver(() => {
|
|
const target = document.getElementById(id);
|
|
if (target) {
|
|
const io = new IntersectionObserver((entries) => {
|
|
const visible = entries.some((e) => e.isIntersecting);
|
|
if (visible) {
|
|
observer.disconnect();
|
|
io.disconnect();
|
|
resolve(target);
|
|
}
|
|
});
|
|
io.observe(target);
|
|
}
|
|
});
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
});
|
|
}
|
|
|
|
// src/helpers/getClashApiUrl.ts
|
|
function getClashApiUrl() {
|
|
const { hostname } = window.location;
|
|
return `http://${hostname}:9090`;
|
|
}
|
|
function getClashWsUrl() {
|
|
const { hostname } = window.location;
|
|
return `ws://${hostname}:9090`;
|
|
}
|
|
|
|
// src/helpers/splitProxyString.ts
|
|
function splitProxyString(str) {
|
|
return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean);
|
|
}
|
|
|
|
// src/helpers/preserveScrollForPage.ts
|
|
function preserveScrollForPage(renderFn) {
|
|
const scrollY = window.scrollY;
|
|
renderFn();
|
|
requestAnimationFrame(() => {
|
|
window.scrollTo({ top: scrollY });
|
|
});
|
|
}
|
|
|
|
// src/clash/methods/createBaseApiRequest.ts
|
|
async function createBaseApiRequest(fetchFn) {
|
|
try {
|
|
const response = await fetchFn();
|
|
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/clash/methods/getConfig.ts
|
|
async function getClashConfig() {
|
|
return createBaseApiRequest(
|
|
() => fetch(`${getClashApiUrl()}/configs`, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
}
|
|
|
|
// src/clash/methods/getGroupDelay.ts
|
|
async function getClashGroupDelay(group, url = "https://www.gstatic.com/generate_204", timeout = 2e3) {
|
|
const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
|
|
url
|
|
)}&timeout=${timeout}`;
|
|
return createBaseApiRequest(
|
|
() => fetch(endpoint, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
}
|
|
|
|
// src/clash/methods/getProxies.ts
|
|
async function getClashProxies() {
|
|
return createBaseApiRequest(
|
|
() => fetch(`${getClashApiUrl()}/proxies`, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
}
|
|
|
|
// src/clash/methods/getVersion.ts
|
|
async function getClashVersion() {
|
|
return createBaseApiRequest(
|
|
() => fetch(`${getClashApiUrl()}/version`, {
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
})
|
|
);
|
|
}
|
|
|
|
// src/clash/methods/triggerProxySelector.ts
|
|
async function triggerProxySelector(selector, outbound) {
|
|
return createBaseApiRequest(
|
|
() => fetch(`${getClashApiUrl()}/proxies/${selector}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: outbound })
|
|
})
|
|
);
|
|
}
|
|
|
|
// src/clash/methods/triggerLatencyTest.ts
|
|
async function triggerLatencyGroupTest(tag, timeout = 5e3, url = "https://www.gstatic.com/generate_204") {
|
|
return createBaseApiRequest(
|
|
() => fetch(
|
|
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
|
{
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
}
|
|
)
|
|
);
|
|
}
|
|
async function triggerLatencyProxyTest(tag, timeout = 2e3, url = "https://www.gstatic.com/generate_204") {
|
|
return createBaseApiRequest(
|
|
() => fetch(
|
|
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
|
{
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" }
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// src/podkop/methods/getConfigSections.ts
|
|
async function getConfigSections() {
|
|
return uci.load("podkop").then(() => uci.sections("podkop"));
|
|
}
|
|
|
|
// src/podkop/methods/getDashboardSections.ts
|
|
async function getDashboardSections() {
|
|
const configSections = await getConfigSections();
|
|
const clashProxies = await getClashProxies();
|
|
if (!clashProxies.success) {
|
|
return {
|
|
success: false,
|
|
data: []
|
|
};
|
|
}
|
|
const proxies = Object.entries(clashProxies.data.proxies).map(
|
|
([key, value]) => ({
|
|
code: key,
|
|
value
|
|
})
|
|
);
|
|
const data = configSections.filter((section) => section.mode !== "block").map((section) => {
|
|
if (section.mode === "proxy") {
|
|
if (section.proxy_config_type === "url") {
|
|
const outbound = proxies.find(
|
|
(proxy) => proxy.code === `${section[".name"]}-out`
|
|
);
|
|
const activeConfigs = splitProxyString(section.proxy_string);
|
|
const proxyDisplayName = getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || "";
|
|
return {
|
|
withTagSelect: false,
|
|
code: outbound?.code || section[".name"],
|
|
displayName: section[".name"],
|
|
outbounds: [
|
|
{
|
|
code: outbound?.code || section[".name"],
|
|
displayName: proxyDisplayName,
|
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
|
type: outbound?.value?.type || "",
|
|
selected: true
|
|
}
|
|
]
|
|
};
|
|
}
|
|
if (section.proxy_config_type === "outbound") {
|
|
const outbound = proxies.find(
|
|
(proxy) => proxy.code === `${section[".name"]}-out`
|
|
);
|
|
const parsedOutbound = JSON.parse(section.outbound_json);
|
|
const parsedTag = parsedOutbound?.tag ? decodeURIComponent(parsedOutbound?.tag) : void 0;
|
|
const proxyDisplayName = parsedTag || outbound?.value?.name || "";
|
|
return {
|
|
withTagSelect: false,
|
|
code: outbound?.code || section[".name"],
|
|
displayName: section[".name"],
|
|
outbounds: [
|
|
{
|
|
code: outbound?.code || section[".name"],
|
|
displayName: proxyDisplayName,
|
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
|
type: outbound?.value?.type || "",
|
|
selected: true
|
|
}
|
|
]
|
|
};
|
|
}
|
|
if (section.proxy_config_type === "urltest") {
|
|
const selector = proxies.find(
|
|
(proxy) => proxy.code === `${section[".name"]}-out`
|
|
);
|
|
const outbound = proxies.find(
|
|
(proxy) => proxy.code === `${section[".name"]}-urltest-out`
|
|
);
|
|
const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item, index) => ({
|
|
code: item?.code || "",
|
|
displayName: getProxyUrlName(section.urltest_proxy_links?.[index]) || item?.value?.name || "",
|
|
latency: item?.value?.history?.[0]?.delay || 0,
|
|
type: item?.value?.type || "",
|
|
selected: selector?.value?.now === item?.code
|
|
}));
|
|
return {
|
|
withTagSelect: true,
|
|
code: selector?.code || section[".name"],
|
|
displayName: section[".name"],
|
|
outbounds: [
|
|
{
|
|
code: outbound?.code || "",
|
|
displayName: _("Fastest"),
|
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
|
type: outbound?.value?.type || "",
|
|
selected: selector?.value?.now === outbound?.code
|
|
},
|
|
...outbounds
|
|
]
|
|
};
|
|
}
|
|
}
|
|
if (section.mode === "vpn") {
|
|
const outbound = proxies.find(
|
|
(proxy) => proxy.code === `${section[".name"]}-out`
|
|
);
|
|
return {
|
|
withTagSelect: false,
|
|
code: outbound?.code || section[".name"],
|
|
displayName: section[".name"],
|
|
outbounds: [
|
|
{
|
|
code: outbound?.code || section[".name"],
|
|
displayName: section.interface || outbound?.value?.name || "",
|
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
|
type: outbound?.value?.type || "",
|
|
selected: true
|
|
}
|
|
]
|
|
};
|
|
}
|
|
return {
|
|
withTagSelect: false,
|
|
code: section[".name"],
|
|
displayName: section[".name"],
|
|
outbounds: []
|
|
};
|
|
});
|
|
return {
|
|
success: true,
|
|
data
|
|
};
|
|
}
|
|
|
|
// src/podkop/methods/getPodkopStatus.ts
|
|
async function getPodkopStatus() {
|
|
const response = await executeShellCommand({
|
|
command: "/usr/bin/podkop",
|
|
args: ["get_status"],
|
|
timeout: 1e3
|
|
});
|
|
if (response.stdout) {
|
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
|
}
|
|
return { enabled: 0, status: "unknown" };
|
|
}
|
|
|
|
// src/podkop/methods/getSingboxStatus.ts
|
|
async function getSingboxStatus() {
|
|
const response = await executeShellCommand({
|
|
command: "/usr/bin/podkop",
|
|
args: ["get_sing_box_status"],
|
|
timeout: 1e3
|
|
});
|
|
if (response.stdout) {
|
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
|
}
|
|
return { running: 0, enabled: 0, status: "unknown" };
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
getAllTabs() {
|
|
return this.getTabsInfo();
|
|
}
|
|
getActiveTab() {
|
|
return this.getActiveTabId();
|
|
}
|
|
disconnect() {
|
|
this.observer?.disconnect();
|
|
this.observer = null;
|
|
}
|
|
};
|
|
var TabServiceInstance = TabService.getInstance();
|
|
|
|
// src/store.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 Store = class {
|
|
constructor(initial) {
|
|
this.listeners = /* @__PURE__ */ new Set();
|
|
this.lastHash = "";
|
|
this.value = initial;
|
|
this.initial = structuredClone(initial);
|
|
this.lastHash = jsonStableStringify(initial);
|
|
}
|
|
get() {
|
|
return this.value;
|
|
}
|
|
set(next) {
|
|
const prev = this.value;
|
|
const merged = { ...prev, ...next };
|
|
if (jsonEqual(prev, merged)) return;
|
|
this.value = merged;
|
|
this.lastHash = jsonStableStringify(merged);
|
|
const diff = {};
|
|
for (const key in merged) {
|
|
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
|
}
|
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
|
}
|
|
reset() {
|
|
const prev = this.value;
|
|
const next = structuredClone(this.initial);
|
|
if (jsonEqual(prev, next)) return;
|
|
this.value = next;
|
|
this.lastHash = jsonStableStringify(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, podkop: 0 }
|
|
},
|
|
sectionsWidget: {
|
|
loading: true,
|
|
failed: false,
|
|
latencyFetching: false,
|
|
data: []
|
|
}
|
|
};
|
|
var store = new Store(initialStore);
|
|
|
|
// src/podkop/services/core.service.ts
|
|
function coreService() {
|
|
TabServiceInstance.onChange((activeId, tabs) => {
|
|
store.set({
|
|
tabService: {
|
|
current: activeId || "",
|
|
all: tabs.map((tab) => tab.id)
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// src/podkop/tabs/dashboard/renderSections.ts
|
|
function renderFailedState() {
|
|
return E(
|
|
"div",
|
|
{
|
|
class: "pdk_dashboard-page__outbound-section centered",
|
|
style: "height: 127px"
|
|
},
|
|
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 renderDefaultState({
|
|
section,
|
|
onChooseOutbound,
|
|
onTestLatency,
|
|
latencyFetching
|
|
}) {
|
|
function testLatency() {
|
|
if (section.withTagSelect) {
|
|
return onTestLatency(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";
|
|
}
|
|
return E(
|
|
"div",
|
|
{
|
|
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""} ${section.withTagSelect ? "pdk_dashboard-page__outbound-grid__item--selectable" : ""}`,
|
|
click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code)
|
|
},
|
|
[
|
|
E("b", {}, outbound.displayName),
|
|
E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [
|
|
E(
|
|
"div",
|
|
{ class: "pdk_dashboard-page__outbound-grid__item__type" },
|
|
outbound.type
|
|
),
|
|
E(
|
|
"div",
|
|
{ class: getLatencyClass() },
|
|
outbound.latency ? `${outbound.latency}ms` : "N/A"
|
|
)
|
|
])
|
|
]
|
|
);
|
|
}
|
|
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
|
|
),
|
|
latencyFetching ? E("div", { class: "skeleton", style: "width: 99px; height: 28px" }) : E(
|
|
"button",
|
|
{
|
|
class: "btn dashboard-sections-grid-item-test-latency",
|
|
click: () => testLatency()
|
|
},
|
|
_("Test latency")
|
|
)
|
|
]),
|
|
E(
|
|
"div",
|
|
{ class: "pdk_dashboard-page__outbound-grid" },
|
|
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/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/renderDashboard.ts
|
|
function renderDashboard() {
|
|
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: "",
|
|
displayName: "",
|
|
outbounds: [],
|
|
withTagSelect: false
|
|
},
|
|
onTestLatency: () => {
|
|
},
|
|
onChooseOutbound: () => {
|
|
}
|
|
})
|
|
)
|
|
]
|
|
);
|
|
}
|
|
|
|
// src/socket.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;
|
|
}
|
|
connect(url) {
|
|
if (this.sockets.has(url)) return;
|
|
const ws = new WebSocket(url);
|
|
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);
|
|
console.info(`Connected: ${url}`);
|
|
});
|
|
ws.addEventListener("message", (event) => {
|
|
const handlers = this.listeners.get(url);
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
try {
|
|
handler(event.data);
|
|
} catch (err) {
|
|
console.error(`Handler error for ${url}:`, err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
ws.addEventListener("close", () => {
|
|
this.connected.set(url, false);
|
|
console.warn(`Disconnected: ${url}`);
|
|
this.triggerError(url, "Connection closed");
|
|
});
|
|
ws.addEventListener("error", (err) => {
|
|
console.error(`Socket error for ${url}:`, err);
|
|
this.triggerError(url, err);
|
|
});
|
|
}
|
|
subscribe(url, listener, onError) {
|
|
if (!this.sockets.has(url)) {
|
|
this.connect(url);
|
|
}
|
|
this.listeners.get(url)?.add(listener);
|
|
if (onError) {
|
|
this.errorListeners.get(url)?.add(onError);
|
|
}
|
|
}
|
|
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 {
|
|
console.warn(`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) {
|
|
console.error(`Error handler threw for ${url}:`, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var socket = SocketManager.getInstance();
|
|
|
|
// 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/initDashboardController.ts
|
|
async function fetchDashboardSections() {
|
|
const prev = store.get().sectionsWidget;
|
|
store.set({
|
|
sectionsWidget: {
|
|
...prev,
|
|
failed: false
|
|
}
|
|
});
|
|
const { data, success } = await getDashboardSections();
|
|
store.set({
|
|
sectionsWidget: {
|
|
latencyFetching: false,
|
|
loading: false,
|
|
failed: !success,
|
|
data
|
|
}
|
|
});
|
|
}
|
|
async function fetchServicesInfo() {
|
|
const [podkop, singbox] = await Promise.all([
|
|
getPodkopStatus(),
|
|
getSingboxStatus()
|
|
]);
|
|
store.set({
|
|
servicesInfoWidget: {
|
|
loading: false,
|
|
failed: false,
|
|
data: { singbox: singbox.running, podkop: podkop.enabled }
|
|
}
|
|
});
|
|
}
|
|
async function connectToClashSockets() {
|
|
socket.subscribe(
|
|
`${getClashWsUrl()}/traffic?token=`,
|
|
(msg) => {
|
|
const parsedMsg = JSON.parse(msg);
|
|
store.set({
|
|
bandwidthWidget: {
|
|
loading: false,
|
|
failed: false,
|
|
data: { up: parsedMsg.up, down: parsedMsg.down }
|
|
}
|
|
});
|
|
},
|
|
(_err) => {
|
|
store.set({
|
|
bandwidthWidget: {
|
|
loading: false,
|
|
failed: true,
|
|
data: { up: 0, down: 0 }
|
|
}
|
|
});
|
|
}
|
|
);
|
|
socket.subscribe(
|
|
`${getClashWsUrl()}/connections?token=`,
|
|
(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) => {
|
|
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(selector, tag) {
|
|
await triggerProxySelector(selector, tag);
|
|
await fetchDashboardSections();
|
|
}
|
|
async function handleTestGroupLatency(tag) {
|
|
store.set({
|
|
sectionsWidget: {
|
|
...store.get().sectionsWidget,
|
|
latencyFetching: true
|
|
}
|
|
});
|
|
await triggerLatencyGroupTest(tag);
|
|
await fetchDashboardSections();
|
|
store.set({
|
|
sectionsWidget: {
|
|
...store.get().sectionsWidget,
|
|
latencyFetching: false
|
|
}
|
|
});
|
|
}
|
|
async function handleTestProxyLatency(tag) {
|
|
store.set({
|
|
sectionsWidget: {
|
|
...store.get().sectionsWidget,
|
|
latencyFetching: true
|
|
}
|
|
});
|
|
await triggerLatencyProxyTest(tag);
|
|
await fetchDashboardSections();
|
|
store.set({
|
|
sectionsWidget: {
|
|
...store.get().sectionsWidget,
|
|
latencyFetching: false
|
|
}
|
|
});
|
|
}
|
|
async function renderSectionsWidget() {
|
|
console.log("renderSectionsWidget");
|
|
const sectionsWidget = store.get().sectionsWidget;
|
|
const container = document.getElementById("dashboard-sections-grid");
|
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
|
const renderedWidget = renderSections({
|
|
loading: sectionsWidget.loading,
|
|
failed: sectionsWidget.failed,
|
|
section: {
|
|
code: "",
|
|
displayName: "",
|
|
outbounds: [],
|
|
withTagSelect: false
|
|
},
|
|
onTestLatency: () => {
|
|
},
|
|
onChooseOutbound: () => {
|
|
},
|
|
latencyFetching: sectionsWidget.latencyFetching
|
|
});
|
|
return preserveScrollForPage(() => {
|
|
container.replaceChildren(renderedWidget);
|
|
});
|
|
}
|
|
const renderedWidgets = sectionsWidget.data.map(
|
|
(section) => renderSections({
|
|
loading: sectionsWidget.loading,
|
|
failed: sectionsWidget.failed,
|
|
section,
|
|
latencyFetching: sectionsWidget.latencyFetching,
|
|
onTestLatency: (tag) => {
|
|
if (section.withTagSelect) {
|
|
return handleTestGroupLatency(tag);
|
|
}
|
|
return handleTestProxyLatency(tag);
|
|
},
|
|
onChooseOutbound: (selector, tag) => {
|
|
handleChooseOutbound(selector, tag);
|
|
}
|
|
})
|
|
);
|
|
return preserveScrollForPage(() => {
|
|
container.replaceChildren(...renderedWidgets);
|
|
});
|
|
}
|
|
async function renderBandwidthWidget() {
|
|
console.log("renderBandwidthWidget");
|
|
const traffic = store.get().bandwidthWidget;
|
|
const container = document.getElementById("dashboard-widget-traffic");
|
|
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() {
|
|
console.log("renderTrafficTotalWidget");
|
|
const trafficTotalWidget = store.get().trafficTotalWidget;
|
|
const container = document.getElementById("dashboard-widget-traffic-total");
|
|
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() {
|
|
console.log("renderSystemInfoWidget");
|
|
const systemInfoWidget = store.get().systemInfoWidget;
|
|
const container = document.getElementById("dashboard-widget-system-info");
|
|
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() {
|
|
console.log("renderServicesInfoWidget");
|
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
|
const container = document.getElementById("dashboard-widget-service-info");
|
|
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"),
|
|
value: servicesInfoWidget.data.podkop ? _("\u2714 Enabled") : _("\u2718 Disabled"),
|
|
attributes: {
|
|
class: servicesInfoWidget.data.podkop ? "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 initDashboardController() {
|
|
onMount("dashboard-status").then(() => {
|
|
store.unsubscribe(onStoreUpdate);
|
|
store.reset();
|
|
store.subscribe(onStoreUpdate);
|
|
fetchDashboardSections();
|
|
fetchServicesInfo();
|
|
connectToClashSockets();
|
|
});
|
|
}
|
|
return baseclass.extend({
|
|
ALLOWED_WITH_RUSSIA_INSIDE,
|
|
BOOTSTRAP_DNS_SERVER_OPTIONS,
|
|
BUTTON_FEEDBACK_TIMEOUT,
|
|
CACHE_TIMEOUT,
|
|
COMMAND_SCHEDULING,
|
|
COMMAND_TIMEOUT,
|
|
DIAGNOSTICS_INITIAL_DELAY,
|
|
DIAGNOSTICS_UPDATE_INTERVAL,
|
|
DNS_SERVER_OPTIONS,
|
|
DOMAIN_LIST_OPTIONS,
|
|
ERROR_POLL_INTERVAL,
|
|
FAKEIP_CHECK_DOMAIN,
|
|
FETCH_TIMEOUT,
|
|
IP_CHECK_DOMAIN,
|
|
PODKOP_LUCI_APP_VERSION,
|
|
REGIONAL_OPTIONS,
|
|
STATUS_COLORS,
|
|
TabService,
|
|
TabServiceInstance,
|
|
UPDATE_INTERVAL_OPTIONS,
|
|
bulkValidate,
|
|
coreService,
|
|
createBaseApiRequest,
|
|
executeShellCommand,
|
|
getBaseUrl,
|
|
getClashApiUrl,
|
|
getClashConfig,
|
|
getClashGroupDelay,
|
|
getClashProxies,
|
|
getClashVersion,
|
|
getClashWsUrl,
|
|
getConfigSections,
|
|
getDashboardSections,
|
|
getPodkopStatus,
|
|
getProxyUrlName,
|
|
getSingboxStatus,
|
|
initDashboardController,
|
|
injectGlobalStyles,
|
|
maskIP,
|
|
onMount,
|
|
parseValueList,
|
|
preserveScrollForPage,
|
|
renderDashboard,
|
|
splitProxyString,
|
|
triggerLatencyGroupTest,
|
|
triggerLatencyProxyTest,
|
|
triggerProxySelector,
|
|
validateDNS,
|
|
validateDomain,
|
|
validateIPV4,
|
|
validateOutboundJson,
|
|
validatePath,
|
|
validateProxyUrl,
|
|
validateShadowsocksUrl,
|
|
validateSubnet,
|
|
validateTrojanUrl,
|
|
validateUrl,
|
|
validateVlessUrl,
|
|
withTimeout
|
|
});
|