Kieran's opinionated (and probably slightly dumb) nix config
1<!DOCTYPE html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>🚇 bore</title> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 } 14 15 body { 16 font-family: 'SF Mono', 'Monaco', monospace; 17 background: #0d1117; 18 color: #e6edf3; 19 padding: 2rem; 20 line-height: 1.5; 21 } 22 23 .container { 24 max-width: 1200px; 25 margin: 0 auto; 26 } 27 28 header { 29 margin-bottom: 3rem; 30 } 31 32 h1 { 33 font-size: 2.5rem; 34 background: linear-gradient(135deg, #14b8a6, #fb923c); 35 -webkit-background-clip: text; 36 -webkit-text-fill-color: transparent; 37 margin-bottom: 0.5rem; 38 } 39 40 .subtitle { 41 color: #8b949e; 42 font-size: 0.95rem; 43 } 44 45 .stats { 46 display: grid; 47 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 48 gap: 1rem; 49 margin-bottom: 2rem; 50 } 51 52 .stat-card { 53 background: #161b22; 54 border: 1px solid #30363d; 55 border-radius: 0; 56 padding: 1.5rem; 57 } 58 59 .stat-label { 60 color: #8b949e; 61 font-size: 0.85rem; 62 margin-bottom: 0.5rem; 63 } 64 65 .stat-value { 66 font-size: 2rem; 67 font-weight: 600; 68 color: #14b8a6; 69 } 70 71 .stat-value.orange { 72 color: #fb923c; 73 } 74 75 .section { 76 background: #161b22; 77 border: 1px solid #30363d; 78 border-radius: 0; 79 padding: 1.5rem; 80 margin-bottom: 2rem; 81 } 82 83 h2 { 84 color: #14b8a6; 85 font-size: 1.2rem; 86 margin-bottom: 1.5rem; 87 display: flex; 88 align-items: center; 89 gap: 0.5rem; 90 } 91 92 .tunnel-list { 93 display: flex; 94 flex-direction: column; 95 gap: 1rem; 96 } 97 98 .tunnel { 99 background: #0d1117; 100 border: 1px solid #30363d; 101 border-radius: 0; 102 padding: 1rem; 103 display: grid; 104 grid-template-columns: 1fr auto; 105 gap: 1rem; 106 align-items: center; 107 } 108 109 .tunnel-icon { 110 display: none; 111 } 112 113 .tunnel-info { 114 flex: 1; 115 } 116 117 .tunnel-name { 118 color: #e6edf3; 119 font-weight: 600; 120 margin-bottom: 0.25rem; 121 } 122 123 .tunnel-url { 124 color: #8b949e; 125 font-size: 0.85rem; 126 } 127 128 .tunnel-url a { 129 color: #14b8a6; 130 text-decoration: none; 131 } 132 133 .tunnel-url a:hover { 134 text-decoration: underline; 135 } 136 137 .tunnel-status { 138 padding: 0.25rem 0.75rem; 139 border-radius: 0; 140 font-size: 0.8rem; 141 font-weight: 500; 142 } 143 144 .status-online { 145 background: rgba(20, 184, 166, 0.2); 146 color: #14b8a6; 147 border: 1px solid #14b8a6; 148 } 149 150 .empty-state { 151 text-align: center; 152 padding: 3rem 1rem; 153 color: #8b949e; 154 } 155 156 .empty-icon { 157 font-size: 3rem; 158 margin-bottom: 1rem; 159 opacity: 0.5; 160 } 161 162 code { 163 background: #0d1117; 164 padding: 0.2rem 0.5rem; 165 border-radius: 0; 166 color: #fb923c; 167 font-size: 0.9rem; 168 } 169 170 .usage { 171 background: #0d1117; 172 padding: 1rem; 173 border-radius: 0; 174 margin-top: 1rem; 175 } 176 177 .usage pre { 178 color: #8b949e; 179 font-size: 0.9rem; 180 overflow-x: auto; 181 } 182 183 .last-updated { 184 text-align: center; 185 color: #8b949e; 186 font-size: 0.8rem; 187 margin-top: 2rem; 188 } 189 </style> 190</head> 191 192<body> 193 <div class="container"> 194 <header> 195 <h1>🚇 bore</h1> 196 <p class="subtitle">tunnel dashboard • bore.dunkirk.sh</p> 197 </header> 198 199 <div class="stats"> 200 <div class="stat-card"> 201 <div class="stat-label">active tunnels</div> 202 <div class="stat-value" id="activeTunnels"></div> 203 </div> 204 <div class="stat-card"> 205 <div class="stat-label">server status</div> 206 <div class="stat-value orange" id="serverStatus"></div> 207 </div> 208 <div class="stat-card"> 209 <div class="stat-label">active connections</div> 210 <div class="stat-value" id="totalConnections"></div> 211 </div> 212 </div> 213 214 <div class="section"> 215 <h2>~ active tunnels</h2> 216 <div class="tunnel-list" id="tunnelList"> 217 <div class="empty-state"> 218 <div class="empty-icon">🚇</div> 219 <p>no active tunnels</p> 220 </div> 221 </div> 222 </div> 223 224 <div class="last-updated"> 225 last updated: <span id="lastUpdated">never</span> 226 </div> 227 </div> 228 229 <script> 230 let fetchFailCount = 0; 231 const MAX_FAIL_COUNT = 3; 232 233 async function fetchStats() { 234 try { 235 // Fetch server info 236 const serverResponse = await fetch('/api/serverinfo'); 237 if (!serverResponse.ok) throw new Error('API unavailable'); 238 const serverData = await serverResponse.json(); 239 240 // Fetch HTTP proxies (tunnels) 241 const proxiesResponse = await fetch('/api/proxy/http'); 242 const proxiesData = await proxiesResponse.json(); 243 244 // Reset fail count on success 245 fetchFailCount = 0; 246 247 // Update stats 248 document.getElementById('activeTunnels').textContent = serverData.clientCounts || 0; 249 document.getElementById('serverStatus').textContent = 'online'; 250 document.getElementById('totalConnections').textContent = serverData.curConns || 0; 251 252 // Update tunnels list 253 const tunnelList = document.getElementById('tunnelList'); 254 const proxies = proxiesData.proxies || []; 255 256 if (proxies.length === 0) { 257 tunnelList.innerHTML = ` 258 <div class="empty-state"> 259 <div class="empty-icon">🚇</div> 260 <p>no active tunnels</p> 261 </div> 262 `; 263 } else { 264 tunnelList.innerHTML = proxies.map(proxy => { 265 const subdomain = proxy.conf.subdomain; 266 const url = `https://${subdomain}.bore.dunkirk.sh`; 267 const statusClass = proxy.status === 'online' ? 'status-online' : ''; 268 269 return ` 270 <div class="tunnel"> 271 <div class="tunnel-icon">→</div> 272 <div class="tunnel-info"> 273 <div class="tunnel-name">${proxy.name}</div> 274 <div class="tunnel-url"> 275 <a href="${url}" target="_blank">${url}</a> 276 </div> 277 <div style="color: #8b949e; font-size: 0.75rem; margin-top: 0.25rem;"> 278 started: ${proxy.lastStartTime} • traffic in: ${formatBytes(proxy.todayTrafficIn)} • out: ${formatBytes(proxy.todayTrafficOut)} 279 </div> 280 </div> 281 <div class="tunnel-status ${statusClass}">${proxy.status}</div> 282 </div> 283 `; 284 }).join(''); 285 } 286 287 document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString(); 288 } catch (error) { 289 fetchFailCount++; 290 document.getElementById('serverStatus').textContent = 'offline'; 291 console.error('Failed to fetch stats:', error); 292 293 // Reload page if failed multiple times (server might have updated) 294 if (fetchFailCount >= MAX_FAIL_COUNT) { 295 console.log('Multiple fetch failures detected, reloading page...'); 296 window.location.reload(); 297 } 298 } 299 } 300 301 function formatBytes(bytes) { 302 if (bytes === 0) return '0 B'; 303 const k = 1024; 304 const sizes = ['B', 'KB', 'MB', 'GB']; 305 const i = Math.floor(Math.log(bytes) / Math.log(k)); 306 return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; 307 } 308 309 // Fetch immediately and then every 5 seconds 310 fetchStats(); 311 setInterval(fetchStats, 5000); 312 </script> 313</body> 314 315</html>