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>