···
text-decoration: underline;
···
text-decoration: underline;
+
border-top: 1px solid #30363d;
+
justify-content: space-between;
+
.offline-tunnel-stats {
···
const MAX_FAIL_COUNT = 3;
+
let lastProxiesState = null;
async function fetchStats() {
···
document.getElementById('serverStatus').textContent = 'online';
document.getElementById('totalConnections').textContent = serverData.curConns || 0;
+
const tunnelCount = serverData.clientCounts || 0;
+
const totalTraffic = formatBytes((serverData.totalTrafficIn || 0) + (serverData.totalTrafficOut || 0));
+
document.title = tunnelCount > 0
+
? `bore - ${tunnelCount} active • ${totalTraffic}`
+
// Check if tunnel list structure changed
const proxies = proxiesData.proxies || [];
+
const currentState = JSON.stringify(proxies.map(p => ({ name: p.name, status: p.status })));
+
if (currentState !== lastProxiesState) {
+
// Structure changed, rebuild DOM
+
lastProxiesState = currentState;
+
renderTunnelList(proxies);
+
// Structure unchanged, just update data
+
updateTunnelData(proxies);
+
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
+
document.getElementById('serverStatus').textContent = 'offline';
+
console.error('Failed to fetch stats:', error);
+
// Reload page if failed multiple times (server might have updated)
+
if (fetchFailCount >= MAX_FAIL_COUNT) {
+
console.log('Multiple fetch failures detected, reloading page...');
+
window.location.reload();
+
function renderTunnelList(proxies) {
+
const tunnelList = document.getElementById('tunnelList');
+
const onlineTunnels = proxies.filter(p => p.status === 'online');
+
const offlineTunnels = proxies.filter(p => p.status !== 'online');
+
if (onlineTunnels.length === 0 && offlineTunnels.length === 0) {
+
tunnelList.innerHTML = `
+
<div class="empty-state">
+
<div class="empty-icon">🚇</div>
+
<p>no active tunnels</p>
+
// Render online tunnels
+
if (onlineTunnels.length > 0) {
+
html += onlineTunnels.map(proxy => {
+
const subdomain = proxy.conf?.subdomain || 'unknown';
const url = `https://${subdomain}.bore.dunkirk.sh`;
+
<div class="tunnel" data-tunnel="${proxy.name}">
<div class="tunnel-info">
+
<div class="tunnel-name">${proxy.name || 'unnamed'}</div>
+
<a href="${url}" target="_blank" ondblclick="copyToClipboard(event, '${url}')">${url}</a>
<div style="color: #8b949e; font-size: 0.75rem; margin-top: 0.25rem;">
+
started: <span data-start-time="${proxy.lastStartTime || ''}"></span> • traffic in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span>
+
<div class="tunnel-status status-online">online</div>
+
// Render offline tunnels
+
if (offlineTunnels.length > 0) {
+
html += '<div class="offline-tunnels">';
+
html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>';
+
html += offlineTunnels.map(proxy => {
+
<div class="offline-tunnel" data-tunnel="${proxy.name}">
+
<span class="offline-tunnel-name">${proxy.name || 'unnamed'}</span>
+
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span>
+
const subdomain = proxy.conf.subdomain || 'unknown';
+
const url = `https://${subdomain}.bore.dunkirk.sh`;
+
<div class="offline-tunnel" data-tunnel="${proxy.name}">
+
<span class="offline-tunnel-name">${proxy.name || 'unnamed'} → ${url}</span>
+
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span>
+
tunnelList.innerHTML = html;
+
// Update all relative times
+
updateTunnelData(proxies);
+
function updateTunnelData(proxies) {
+
proxies.forEach(proxy => {
+
const trafficInEl = document.querySelector(`[data-traffic-in="${proxy.name}"]`);
+
const trafficOutEl = document.querySelector(`[data-traffic-out="${proxy.name}"]`);
+
if (trafficInEl) trafficInEl.textContent = formatBytes(proxy.todayTrafficIn || 0);
+
if (trafficOutEl) trafficOutEl.textContent = formatBytes(proxy.todayTrafficOut || 0);
+
// Update relative times
function formatBytes(bytes) {
···
document.querySelectorAll('[data-start-time]').forEach(element => {
const timeStr = element.getAttribute('data-start-time');
element.textContent = formatTime(timeStr);
+
function copyToClipboard(event, url) {
+
event.preventDefault();
+
event.stopPropagation();
+
navigator.clipboard.writeText(url).then(() => {
+
const link = event.target;
+
const originalColor = link.style.color;
+
link.style.color = '#fb923c';
+
link.style.color = originalColor;
+
console.error('Failed to copy:', err);