274 lines
8.6 KiB
JavaScript
274 lines
8.6 KiB
JavaScript
function formatBytes(bytes) {
|
|
if (bytes === 0) return "0 B";
|
|
const k = 1024;
|
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
if (days > 0) return `${days}d ${hours}h`;
|
|
if (hours > 0) return `${hours}h ${mins}m`;
|
|
return `${mins}m`;
|
|
}
|
|
|
|
const regionNames =
|
|
typeof Intl !== "undefined" && Intl.DisplayNames
|
|
? new Intl.DisplayNames([navigator.language || "en"], { type: "region" })
|
|
: null;
|
|
|
|
function normalizeCode(countryCode) {
|
|
if (!countryCode || countryCode === "Unknown") return null;
|
|
const normalized = String(countryCode).trim().toUpperCase();
|
|
return normalized.length === 2 ? normalized : null;
|
|
}
|
|
|
|
function getCountryName(countryCode) {
|
|
const normalized = normalizeCode(countryCode);
|
|
if (!normalized) return "Unknown";
|
|
try {
|
|
return regionNames?.of(normalized) || normalized;
|
|
} catch {
|
|
return normalized;
|
|
}
|
|
}
|
|
|
|
function getFlag(countryCode) {
|
|
const normalized = normalizeCode(countryCode);
|
|
if (!normalized) return "🌐";
|
|
const codePoints = normalized
|
|
.split("")
|
|
.map((char) => 127397 + char.charCodeAt(0));
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
|
|
function formatKB(kbString) {
|
|
const kb = parseFloat(kbString);
|
|
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(2)} GB`;
|
|
if (kb >= 1024) return `${(kb / 1024).toFixed(2)} MB`;
|
|
return `${kb.toFixed(2)} KB`;
|
|
}
|
|
|
|
async function fetchLogs() {
|
|
try {
|
|
const response = await fetch("/api/logs");
|
|
const text = await response.text();
|
|
|
|
const logDiv = document.getElementById("activity-log");
|
|
const lines = text
|
|
.split("\n")
|
|
.filter((line) => line.includes("In the last"))
|
|
.reverse();
|
|
|
|
if (lines.length === 0) {
|
|
logDiv.innerHTML =
|
|
'<div class="loading">No activity yet. Check back soon!</div>';
|
|
return;
|
|
}
|
|
|
|
const rows = lines
|
|
.flatMap((line) => {
|
|
const match = line.match(
|
|
/(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}).*?(\d+) completed.*?↓ ([\d.]+) KB \(([\d.]+) KB\/s\).*?↑ ([\d.]+) KB \(([\d.]+) KB\/s\)/,
|
|
);
|
|
if (!match) return [];
|
|
|
|
const [, timestamp, connections, dlTotal, dlRate, ulTotal, ulRate] =
|
|
match;
|
|
const utcDate = new Date(`${timestamp.replace(/\//g, "-")}Z`);
|
|
const time = utcDate.toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
const fullDate = utcDate.toLocaleDateString([], {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
|
|
const isoDate = utcDate.toISOString();
|
|
return [
|
|
`<tr>
|
|
<td><time datetime="${isoDate}" aria-label="${fullDate}">${time}</time></td>
|
|
<td>${connections}</td>
|
|
<td>${formatKB(dlTotal)} <span class="rate">(${formatKB(dlRate)}/s)</span></td>
|
|
<td>${formatKB(ulTotal)} <span class="rate">(${formatKB(ulRate)}/s)</span></td>
|
|
</tr>`,
|
|
];
|
|
})
|
|
.join("");
|
|
|
|
logDiv.innerHTML = `<table class="activity-table" aria-labelledby="activity-heading">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Time</th>
|
|
<th scope="col">Conns</th>
|
|
<th scope="col">Download</th>
|
|
<th scope="col">Upload</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows}
|
|
</tbody>
|
|
</table>`;
|
|
pulse(logDiv);
|
|
} catch (_e) {
|
|
document.getElementById("activity-log").innerHTML =
|
|
'<div class="error">Error loading logs</div>';
|
|
}
|
|
}
|
|
|
|
async function fetchNatType() {
|
|
try {
|
|
const response = await fetch("/api/nat");
|
|
const text = await response.text();
|
|
const natType = text.trim() || "Unknown";
|
|
const el = document.getElementById("nat-type");
|
|
const row = document.getElementById("nat-row");
|
|
|
|
const isUnknown = !natType || natType.toLowerCase() === "unknown";
|
|
if (row) row.hidden = isUnknown;
|
|
if (el && !isUnknown) {
|
|
const capitalized =
|
|
natType.charAt(0).toUpperCase() + natType.slice(1).toLowerCase();
|
|
el.textContent = capitalized;
|
|
|
|
const natLower = natType.toLowerCase();
|
|
let natDesc = "";
|
|
if (natLower === "unrestricted") {
|
|
natDesc =
|
|
"Address-independent NAT mapping. Compatible with most other NATs and can connect to restricted proxies.";
|
|
} else if (natLower === "restricted") {
|
|
natDesc =
|
|
"Address-dependent NAT mapping. May have limited compatibility with other restricted NATs.";
|
|
}
|
|
if (natDesc) {
|
|
el.title = natDesc;
|
|
const descId = "nat-type-desc";
|
|
let descEl = document.getElementById(descId);
|
|
if (!descEl) {
|
|
descEl = document.createElement("span");
|
|
descEl.id = descId;
|
|
descEl.className = "sr-only";
|
|
el.insertAdjacentElement("afterend", descEl);
|
|
}
|
|
descEl.textContent = natDesc;
|
|
el.setAttribute("aria-describedby", descId);
|
|
}
|
|
}
|
|
} catch (_e) {
|
|
const el = document.getElementById("nat-type");
|
|
const row = document.getElementById("nat-row");
|
|
if (row) row.hidden = false;
|
|
if (el) el.innerHTML = '<span class="error">Error</span>';
|
|
}
|
|
}
|
|
|
|
async function fetchStats() {
|
|
try {
|
|
const response = await fetch("/api/metrics");
|
|
const text = await response.text();
|
|
|
|
let total = 0;
|
|
let timeouts = 0;
|
|
let torInbound = 0;
|
|
let torOutbound = 0;
|
|
let memory = 0;
|
|
let startTime = 0;
|
|
const countries = {};
|
|
|
|
text.split("\n").forEach((line) => {
|
|
if (line.startsWith("tor_snowflake_proxy_connections_total")) {
|
|
const match = line.match(/country="([^"]*)".*?(\d+)$/);
|
|
if (match) {
|
|
const country = match[1] || "Unknown";
|
|
const count = parseInt(match[2], 10);
|
|
countries[country] = count;
|
|
total += count;
|
|
}
|
|
} else if (
|
|
line.startsWith("tor_snowflake_proxy_connection_timeouts_total")
|
|
) {
|
|
const match = line.match(/(\d+)$/);
|
|
if (match) timeouts = parseInt(match[1], 10);
|
|
} else if (
|
|
line.startsWith("tor_snowflake_proxy_traffic_inbound_bytes_total")
|
|
) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) torInbound = parseFloat(match[1]) * 1024;
|
|
} else if (
|
|
line.startsWith("tor_snowflake_proxy_traffic_outbound_bytes_total")
|
|
) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) torOutbound = parseFloat(match[1]) * 1024;
|
|
} else if (line.startsWith("process_resident_memory_bytes")) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) memory = parseFloat(match[1]);
|
|
} else if (line.startsWith("process_start_time_seconds")) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) startTime = parseFloat(match[1]);
|
|
}
|
|
});
|
|
|
|
const attempts = total + timeouts;
|
|
const successRate = attempts > 0 ? (total / attempts) * 100 : 0;
|
|
const uptime = startTime > 0 ? Date.now() / 1000 - startTime : 0;
|
|
|
|
document.getElementById("total").textContent = total;
|
|
document.getElementById("timeouts").textContent = timeouts;
|
|
const rateEl = document.getElementById("success-rate-small");
|
|
rateEl.textContent = `${successRate.toFixed(1)}%`;
|
|
rateEl.className = "";
|
|
document.getElementById("tor-traffic").textContent =
|
|
`${formatBytes(torInbound)} / ${formatBytes(torOutbound)}`;
|
|
document.getElementById("memory").textContent = formatBytes(memory);
|
|
document.getElementById("uptime").textContent = formatUptime(uptime);
|
|
pulse(document.getElementById("metrics-stat"));
|
|
|
|
const table = document.getElementById("countries");
|
|
const tbody = table.querySelector("tbody");
|
|
tbody.innerHTML = "";
|
|
Object.entries(countries)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.forEach(([country, count]) => {
|
|
const row = tbody.insertRow();
|
|
const displayName = getCountryName(country);
|
|
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : 0;
|
|
|
|
const countryCell = row.insertCell(0);
|
|
const flagSpan = document.createElement("span");
|
|
flagSpan.className = "flag";
|
|
flagSpan.textContent = getFlag(country);
|
|
countryCell.appendChild(flagSpan);
|
|
countryCell.appendChild(document.createTextNode(displayName));
|
|
|
|
row.insertCell(1).textContent = count;
|
|
row.insertCell(2).textContent = `${percentage}%`;
|
|
});
|
|
} catch (_e) {
|
|
for (const id of ["total", "timeouts", "tor-traffic", "memory", "uptime"]) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.innerHTML = '<span class="error">Error</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
function pulse(el) {
|
|
if (!el) return;
|
|
el.classList.remove("refreshed");
|
|
// force reflow so the animation restarts
|
|
void el.offsetWidth;
|
|
el.classList.add("refreshed");
|
|
}
|
|
|
|
fetchStats();
|
|
fetchLogs();
|
|
fetchNatType();
|
|
setInterval(fetchStats, 300000);
|
|
setInterval(fetchLogs, 300000);
|