···
text-decoration: underline;
146
+
.tunnel-url a:active {
···
text-decoration: underline;
214
+
margin-top: 1.5rem;
215
+
padding-top: 1.5rem;
216
+
border-top: 1px solid #30363d;
222
+
font-size: 0.85rem;
224
+
justify-content: space-between;
225
+
align-items: center;
228
+
.offline-tunnel-name {
232
+
.offline-tunnel-stats {
233
+
font-size: 0.75rem;
···
const MAX_FAIL_COUNT = 3;
280
+
let lastProxiesState = null;
async function fetchStats() {
···
document.getElementById('serverStatus').textContent = 'online';
document.getElementById('totalConnections').textContent = serverData.curConns || 0;
270
-
// Update tunnels list
271
-
const tunnelList = document.getElementById('tunnelList');
301
+
// Update page title
302
+
const tunnelCount = serverData.clientCounts || 0;
303
+
const totalTraffic = formatBytes((serverData.totalTrafficIn || 0) + (serverData.totalTrafficOut || 0));
304
+
document.title = tunnelCount > 0
305
+
? `bore - ${tunnelCount} active • ${totalTraffic}`
308
+
// Check if tunnel list structure changed
const proxies = proxiesData.proxies || [];
310
+
const currentState = JSON.stringify(proxies.map(p => ({ name: p.name, status: p.status })));
312
+
if (currentState !== lastProxiesState) {
313
+
// Structure changed, rebuild DOM
314
+
lastProxiesState = currentState;
315
+
renderTunnelList(proxies);
317
+
// Structure unchanged, just update data
318
+
updateTunnelData(proxies);
274
-
if (proxies.length === 0) {
275
-
tunnelList.innerHTML = `
276
-
<div class="empty-state">
277
-
<div class="empty-icon">🚇</div>
278
-
<p>no active tunnels</p>
282
-
tunnelList.innerHTML = proxies.map(proxy => {
283
-
const subdomain = proxy.conf.subdomain;
321
+
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
324
+
document.getElementById('serverStatus').textContent = 'offline';
325
+
console.error('Failed to fetch stats:', error);
327
+
// Reload page if failed multiple times (server might have updated)
328
+
if (fetchFailCount >= MAX_FAIL_COUNT) {
329
+
console.log('Multiple fetch failures detected, reloading page...');
330
+
window.location.reload();
335
+
function renderTunnelList(proxies) {
336
+
const tunnelList = document.getElementById('tunnelList');
337
+
const onlineTunnels = proxies.filter(p => p.status === 'online');
338
+
const offlineTunnels = proxies.filter(p => p.status !== 'online');
340
+
if (onlineTunnels.length === 0 && offlineTunnels.length === 0) {
341
+
tunnelList.innerHTML = `
342
+
<div class="empty-state">
343
+
<div class="empty-icon">🚇</div>
344
+
<p>no active tunnels</p>
350
+
// Render online tunnels
351
+
if (onlineTunnels.length > 0) {
352
+
html += onlineTunnels.map(proxy => {
353
+
const subdomain = proxy.conf?.subdomain || 'unknown';
const url = `https://${subdomain}.bore.dunkirk.sh`;
285
-
const statusClass = proxy.status === 'online' ? 'status-online' : '';
288
-
<div class="tunnel">
289
-
<div class="tunnel-icon">→</div>
357
+
<div class="tunnel" data-tunnel="${proxy.name}">
<div class="tunnel-info">
291
-
<div class="tunnel-name">${proxy.name}</div>
359
+
<div class="tunnel-name">${proxy.name || 'unnamed'}</div>
293
-
<a href="${url}" target="_blank">${url}</a>
361
+
<a href="${url}" target="_blank" ondblclick="copyToClipboard(event, '${url}')">${url}</a>
<div style="color: #8b949e; font-size: 0.75rem; margin-top: 0.25rem;">
296
-
started: <span data-start-time="${proxy.lastStartTime}"></span> • traffic in: ${formatBytes(proxy.todayTrafficIn)} • out: ${formatBytes(proxy.todayTrafficOut)}
364
+
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>
299
-
<div class="tunnel-status ${statusClass}">${proxy.status}</div>
367
+
<div class="tunnel-status status-online">online</div>
304
-
// Update all relative times
305
-
updateRelativeTimes();
373
+
// Render offline tunnels
374
+
if (offlineTunnels.length > 0) {
375
+
html += '<div class="offline-tunnels">';
376
+
html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>';
377
+
html += offlineTunnels.map(proxy => {
380
+
<div class="offline-tunnel" data-tunnel="${proxy.name}">
381
+
<span class="offline-tunnel-name">${proxy.name || 'unnamed'}</span>
382
+
<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>
387
+
const subdomain = proxy.conf.subdomain || 'unknown';
388
+
const url = `https://${subdomain}.bore.dunkirk.sh`;
390
+
<div class="offline-tunnel" data-tunnel="${proxy.name}">
391
+
<span class="offline-tunnel-name">${proxy.name || 'unnamed'} → ${url}</span>
392
+
<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>
399
+
tunnelList.innerHTML = html;
308
-
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
311
-
document.getElementById('serverStatus').textContent = 'offline';
312
-
console.error('Failed to fetch stats:', error);
401
+
// Update all relative times
402
+
updateRelativeTimes();
314
-
// Reload page if failed multiple times (server might have updated)
315
-
if (fetchFailCount >= MAX_FAIL_COUNT) {
316
-
console.log('Multiple fetch failures detected, reloading page...');
317
-
window.location.reload();
408
+
updateTunnelData(proxies);
411
+
function updateTunnelData(proxies) {
412
+
proxies.forEach(proxy => {
413
+
const trafficInEl = document.querySelector(`[data-traffic-in="${proxy.name}"]`);
414
+
const trafficOutEl = document.querySelector(`[data-traffic-out="${proxy.name}"]`);
416
+
if (trafficInEl) trafficInEl.textContent = formatBytes(proxy.todayTrafficIn || 0);
417
+
if (trafficOutEl) trafficOutEl.textContent = formatBytes(proxy.todayTrafficOut || 0);
422
+
// Update relative times
423
+
updateRelativeTimes();
function formatBytes(bytes) {
···
document.querySelectorAll('[data-start-time]').forEach(element => {
const timeStr = element.getAttribute('data-start-time');
element.textContent = formatTime(timeStr);
475
+
function copyToClipboard(event, url) {
476
+
event.preventDefault();
477
+
event.stopPropagation();
479
+
navigator.clipboard.writeText(url).then(() => {
481
+
const link = event.target;
482
+
const originalColor = link.style.color;
483
+
link.style.color = '#fb923c';
485
+
link.style.color = originalColor;
488
+
console.error('Failed to copy:', err);