227 lines
11 KiB
HTML
227 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Snowflake Dashboard</title>
|
|
<meta http-equiv="refresh" content="60">
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="grid" style="grid-template-columns: repeat(3, 1fr);">
|
|
<div class="stat">
|
|
<div class="label">Total Connections</div>
|
|
<div class="big" id="total">-</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="label">Connection Timeouts</div>
|
|
<div class="big" id="timeouts">-</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="label">Success Rate</div>
|
|
<div class="big success-rate" id="success-rate">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid" style="grid-template-columns: 1fr 1fr;">
|
|
<div style="display: flex; flex-direction: column; gap: 20px;">
|
|
<div class="stat">
|
|
<div class="label">Network Stats</div>
|
|
<div class="metric-row">
|
|
<span>Total Traffic In / Out</span>
|
|
<span id="total-traffic">-</span>
|
|
</div>
|
|
<div class="metric-row">
|
|
<span>Tor Relay In / Out</span>
|
|
<span id="tor-traffic">-</span>
|
|
</div>
|
|
<div class="metric-row">
|
|
<span>Memory Usage</span>
|
|
<span id="memory">-</span>
|
|
</div>
|
|
<div class="metric-row">
|
|
<span>Uptime</span>
|
|
<span id="uptime">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat">
|
|
<div class="label">Recent Activity (Hourly)</div>
|
|
<div id="activity-log" style="font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.8; margin-top: 15px;">
|
|
<div style="color: var(--text-secondary);">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat">
|
|
<div class="label">Connections by Country</div>
|
|
<table id="countries">
|
|
<tr><th>Country</th><th>Count</th></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
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`;
|
|
}
|
|
|
|
function getFlag(countryCode) {
|
|
if (!countryCode || countryCode === 'Unknown') return '🌐';
|
|
const codePoints = countryCode
|
|
.toUpperCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt());
|
|
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('/internal/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 style="color: var(--text-secondary);">No activity logs available</div>';
|
|
return;
|
|
}
|
|
|
|
const rows = lines.map(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 time = timestamp.split(' ')[1];
|
|
|
|
return `<tr>
|
|
<td>${time}</td>
|
|
<td>${connections}</td>
|
|
<td>${formatKB(dlTotal)} <span style="color: var(--text-secondary); font-size: 12px;">(${formatKB(dlRate)}/s)</span></td>
|
|
<td>${formatKB(ulTotal)} <span style="color: var(--text-secondary); font-size: 12px;">(${formatKB(ulRate)}/s)</span></td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
logDiv.innerHTML = `<table style="width: 100%; margin-top: 10px; font-family: 'Courier New', monospace; font-size: 13px;">
|
|
<tr style="color: var(--text-secondary); font-size: 11px; text-transform: uppercase;">
|
|
<th style="text-align: left; padding: 8px 4px;">Time</th>
|
|
<th style="text-align: left; padding: 8px 4px;">Conns</th>
|
|
<th style="text-align: left; padding: 8px 4px;">Download</th>
|
|
<th style="text-align: left; padding: 8px 4px;">Upload</th>
|
|
</tr>
|
|
${rows}
|
|
</table>`;
|
|
} catch (_e) {
|
|
document.getElementById('activity-log').innerHTML = '<div style="color: var(--error-color);">Error loading logs</div>';
|
|
}
|
|
}
|
|
|
|
async function fetchStats() {
|
|
try {
|
|
const response = await fetch('/internal/metrics');
|
|
const text = await response.text();
|
|
|
|
let total = 0;
|
|
let timeouts = 0;
|
|
let inbound = 0;
|
|
let outbound = 0;
|
|
let torInbound = 0;
|
|
let torOutbound = 0;
|
|
let memory = 0;
|
|
let startTime = 0;
|
|
let _goroutines = 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('process_network_receive_bytes_total')) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) inbound = parseFloat(match[1]);
|
|
} else if (line.startsWith('process_network_transmit_bytes_total')) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) outbound = parseFloat(match[1]);
|
|
} 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]);
|
|
} else if (line.startsWith('go_goroutines')) {
|
|
const match = line.match(/(\d+\.?\d*e?\+?\d*)$/);
|
|
if (match) _goroutines = parseFloat(match[1]);
|
|
}
|
|
});
|
|
|
|
const attempts = total + timeouts;
|
|
const successRate = attempts > 0 ? ((total / attempts) * 100).toFixed(1) : 0;
|
|
const uptime = startTime > 0 ? Date.now() / 1000 - startTime : 0;
|
|
|
|
|
|
document.getElementById('total').textContent = total;
|
|
document.getElementById('timeouts').textContent = timeouts;
|
|
document.getElementById('success-rate').textContent = `${successRate}%`;
|
|
document.getElementById('total-traffic').textContent = `${formatBytes(inbound)} / ${formatBytes(outbound)}`;
|
|
document.getElementById('tor-traffic').textContent = `${formatBytes(torInbound)} / ${formatBytes(torOutbound)}`;
|
|
document.getElementById('memory').textContent = formatBytes(memory);
|
|
document.getElementById('uptime').textContent = formatUptime(uptime);
|
|
|
|
const table = document.getElementById('countries');
|
|
table.innerHTML = '<tr><th>Country</th><th>Count</th></tr>';
|
|
Object.entries(countries).sort((a, b) => b[1] - a[1]).forEach(([country, count]) => {
|
|
const row = table.insertRow();
|
|
const flag = getFlag(country);
|
|
const displayName = country || 'Unknown';
|
|
row.insertCell(0).innerHTML = `<span class="flag">${flag}</span>${displayName}`;
|
|
row.insertCell(1).textContent = count;
|
|
});
|
|
} catch (_e) {
|
|
document.getElementById('total').innerHTML = '<span class="error">Error</span>';
|
|
}
|
|
}
|
|
|
|
fetchStats();
|
|
fetchLogs();
|
|
setInterval(fetchStats, 60000);
|
|
setInterval(fetchLogs, 60000);
|
|
</script>
|
|
</body>
|
|
</html>
|