Wendy-Fly commited on
Commit
9f4df6f
·
verified ·
1 Parent(s): fced2fc

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +5 -10
  2. icon.png +0 -0
  3. icon.svg +10 -0
  4. index.html +463 -0
  5. manifest.json +16 -0
README.md CHANGED
@@ -1,13 +1,8 @@
1
  ---
2
- title: ServerMonitor
3
- emoji: 👁
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.15.2
8
- python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Server Monitor
3
+ emoji: 🖥️
4
+ colorFrom: gray
5
+ colorTo: blue
6
+ sdk: static
 
 
 
7
  pinned: false
8
  ---
 
 
icon.png ADDED
icon.svg ADDED
index.html ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
+ <meta name="theme-color" content="#0a0e17">
9
+ <link rel="manifest" href="manifest.json">
10
+ <link rel="apple-touch-icon" href="icon.png">
11
+ <title>Server Monitor</title>
12
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
13
+ <style>
14
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
15
+ :root{
16
+ --bg:#0a0e17;--surface:#111827;--surface2:#1a2332;--border:#1e2d3d;
17
+ --text:#e2e8f0;--text2:#8b9dc3;--text3:#4a5e78;
18
+ --green:#22c55e;--green-dim:#0a3d1f;--yellow:#eab308;--yellow-dim:#3d3408;
19
+ --red:#ef4444;--red-dim:#3d0a0a;--blue:#3b82f6;--cyan:#06b6d4;
20
+ --glow-green:0 0 20px rgba(34,197,94,.15);
21
+ --glow-red:0 0 20px rgba(239,68,68,.15);
22
+ }
23
+ html,body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;
24
+ min-height:100vh;overflow-x:hidden;-webkit-font-smoothing:antialiased}
25
+
26
+ /* Noise overlay */
27
+ body::before{content:'';position:fixed;inset:0;z-index:9999;pointer-events:none;
28
+ opacity:.03;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
29
+
30
+ /* Scanline */
31
+ body::after{content:'';position:fixed;inset:0;z-index:9998;pointer-events:none;
32
+ background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.03) 2px,rgba(0,0,0,.03) 4px)}
33
+
34
+ .app{max-width:480px;margin:0 auto;padding:16px 16px 100px;position:relative}
35
+
36
+ /* Header */
37
+ .header{display:flex;align-items:center;justify-content:space-between;
38
+ padding:20px 0 24px;border-bottom:1px solid var(--border);margin-bottom:20px}
39
+ .header-left{display:flex;align-items:center;gap:12px}
40
+ .logo{width:36px;height:36px;border-radius:10px;
41
+ background:linear-gradient(135deg,var(--cyan),var(--blue));
42
+ display:flex;align-items:center;justify-content:center;
43
+ font-family:'JetBrains Mono',monospace;font-weight:700;font-size:16px;color:#fff;
44
+ box-shadow:0 0 24px rgba(6,182,212,.2)}
45
+ .header h1{font-size:18px;font-weight:600;letter-spacing:-.02em}
46
+ .header-meta{text-align:right}
47
+ .header-meta .time{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text2)}
48
+ .header-meta .status{font-size:10px;color:var(--green);display:flex;align-items:center;gap:4px;justify-content:flex-end;margin-top:2px}
49
+ .header-meta .status::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--green);
50
+ animation:pulse-dot 2s ease-in-out infinite}
51
+ @keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}
52
+
53
+ .refresh-btn{background:var(--surface);border:1px solid var(--border);border-radius:8px;
54
+ padding:6px 10px;color:var(--text2);font-size:11px;font-family:'JetBrains Mono',monospace;
55
+ cursor:pointer;transition:all .2s}
56
+ .refresh-btn:active{transform:scale(.95);background:var(--surface2)}
57
+
58
+ /* Server Card */
59
+ .server-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;
60
+ padding:20px;margin-bottom:16px;position:relative;overflow:hidden;
61
+ transition:transform .2s,box-shadow .2s;
62
+ animation:card-in .5s cubic-bezier(.16,1,.3,1) backwards}
63
+ .server-card:nth-child(1){animation-delay:.05s}
64
+ .server-card:nth-child(2){animation-delay:.1s}
65
+ .server-card:nth-child(3){animation-delay:.15s}
66
+ @keyframes card-in{from{opacity:0;transform:translateY(16px)}}
67
+
68
+ .server-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
69
+ background:linear-gradient(90deg,var(--cyan),var(--blue));opacity:.6}
70
+ .server-card.warn::before{background:linear-gradient(90deg,var(--yellow),#f59e0b)}
71
+ .server-card.danger::before{background:linear-gradient(90deg,var(--red),#dc2626);
72
+ box-shadow:var(--glow-red)}
73
+
74
+ /* Card Header */
75
+ .card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
76
+ .card-host{display:flex;align-items:center;gap:10px}
77
+ .card-host .dot{width:10px;height:10px;border-radius:50%;background:var(--green);
78
+ box-shadow:var(--glow-green);flex-shrink:0}
79
+ .card-host .dot.offline{background:var(--red);box-shadow:var(--glow-red);animation:blink 1s step-end infinite}
80
+ @keyframes blink{50%{opacity:.3}}
81
+ .card-host h2{font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:600}
82
+ .card-age{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);
83
+ background:var(--surface2);padding:3px 8px;border-radius:6px}
84
+
85
+ /* Stats Grid */
86
+ .stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px}
87
+ .stat{background:var(--surface2);border-radius:10px;padding:10px 12px}
88
+ .stat-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;
89
+ font-family:'JetBrains Mono',monospace;margin-bottom:6px}
90
+ .stat-value{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:600;
91
+ letter-spacing:-.02em}
92
+ .stat-bar{height:3px;background:var(--border);border-radius:2px;margin-top:6px;overflow:hidden}
93
+ .stat-bar-fill{height:100%;border-radius:2px;transition:width .8s cubic-bezier(.16,1,.3,1)}
94
+ .stat-sub{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3);margin-top:3px}
95
+
96
+ /* GPU Section */
97
+ .gpu-section{margin-bottom:14px}
98
+ .gpu-title{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);
99
+ text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;
100
+ display:flex;align-items:center;gap:6px}
101
+ .gpu-title::before{content:'';width:12px;height:1px;background:var(--border)}
102
+
103
+ .gpu-card{background:var(--surface2);border-radius:10px;padding:12px;margin-bottom:6px;
104
+ border:1px solid transparent;transition:border-color .2s}
105
+ .gpu-card.hot{border-color:rgba(239,68,68,.3)}
106
+ .gpu-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
107
+ .gpu-name{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;color:var(--text2)}
108
+ .gpu-temp{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:600}
109
+ .gpu-meters{display:grid;grid-template-columns:1fr 1fr;gap:8px}
110
+ .gpu-meter-label{font-size:9px;color:var(--text3);font-family:'JetBrains Mono',monospace;
111
+ margin-bottom:3px;display:flex;justify-content:space-between}
112
+ .gpu-meter-bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden}
113
+ .gpu-meter-fill{height:100%;border-radius:2px;transition:width .8s cubic-bezier(.16,1,.3,1)}
114
+
115
+ /* Tasks */
116
+ .tasks-section{border-top:1px solid var(--border);padding-top:12px}
117
+ .tasks-title{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);
118
+ text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px}
119
+ .task-item{display:flex;align-items:center;gap:8px;padding:6px 0;
120
+ border-bottom:1px solid rgba(30,45,61,.5)}
121
+ .task-item:last-child{border-bottom:none}
122
+ .task-gpu-badge{font-family:'JetBrains Mono',monospace;font-size:9px;
123
+ background:rgba(59,130,246,.15);color:var(--blue);padding:2px 6px;border-radius:4px;
124
+ flex-shrink:0}
125
+ .task-cmd{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text2);
126
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
127
+ .task-dur{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3);flex-shrink:0}
128
+
129
+ /* Empty state */
130
+ .empty{text-align:center;padding:60px 20px}
131
+ .empty-icon{font-size:40px;margin-bottom:12px;opacity:.3}
132
+ .empty-text{font-size:14px;color:var(--text3)}
133
+
134
+ /* Loading */
135
+ .loading{text-align:center;padding:80px 20px}
136
+ .loading-spinner{width:32px;height:32px;border:2px solid var(--border);
137
+ border-top-color:var(--cyan);border-radius:50%;margin:0 auto 16px;
138
+ animation:spin .8s linear infinite}
139
+ @keyframes spin{to{transform:rotate(360deg)}}
140
+ .loading-text{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--text3)}
141
+
142
+ /* Error */
143
+ .error-banner{background:var(--red-dim);border:1px solid rgba(239,68,68,.3);
144
+ border-radius:10px;padding:12px 16px;margin-bottom:16px;
145
+ font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--red)}
146
+
147
+ /* Color helpers */
148
+ .c-green{color:var(--green)}.c-yellow{color:var(--yellow)}.c-red{color:var(--red)}
149
+ .bg-green{background:var(--green)}.bg-yellow{background:var(--yellow)}.bg-red{background:var(--red)}
150
+ .bg-blue{background:var(--blue)}.bg-cyan{background:var(--cyan)}
151
+
152
+ /* Settings bar */
153
+ .settings-bar{position:fixed;bottom:0;left:0;right:0;
154
+ background:rgba(10,14,23,.92);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
155
+ border-top:1px solid var(--border);padding:12px 16px;z-index:100;
156
+ display:flex;align-items:center;justify-content:center;gap:16px}
157
+ .settings-bar .info{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3)}
158
+ .countdown{color:var(--cyan)}
159
+
160
+ /* Token input */
161
+ .token-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:200;
162
+ display:flex;align-items:center;justify-content:center;padding:20px}
163
+ .token-box{background:var(--surface);border:1px solid var(--border);border-radius:16px;
164
+ padding:28px 24px;width:100%;max-width:400px}
165
+ .token-box h2{font-size:16px;margin-bottom:6px}
166
+ .token-box p{font-size:12px;color:var(--text2);margin-bottom:16px}
167
+ .token-box input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);
168
+ border-radius:8px;color:var(--text);font-family:'JetBrains Mono',monospace;font-size:13px;
169
+ outline:none}
170
+ .token-box input:focus{border-color:var(--cyan)}
171
+ .token-box .btn-row{display:flex;gap:8px;margin-top:12px}
172
+ .token-box button{flex:1;padding:10px;border-radius:8px;font-size:13px;font-weight:600;
173
+ cursor:pointer;border:none;transition:all .15s}
174
+ .btn-primary{background:var(--cyan);color:#000}
175
+ .btn-primary:active{transform:scale(.97)}
176
+ .btn-secondary{background:var(--surface2);color:var(--text2);border:1px solid var(--border) !important}
177
+ </style>
178
+ </head>
179
+ <body>
180
+
181
+ <div class="app" id="app">
182
+ <header class="header">
183
+ <div class="header-left">
184
+ <div class="logo">SM</div>
185
+ <div>
186
+ <h1>Server Monitor</h1>
187
+ </div>
188
+ </div>
189
+ <div class="header-meta">
190
+ <div class="time" id="clock"></div>
191
+ <div class="status" id="status-line">LIVE</div>
192
+ </div>
193
+ </header>
194
+
195
+ <div id="error-container"></div>
196
+ <div id="server-list">
197
+ <div class="loading">
198
+ <div class="loading-spinner"></div>
199
+ <div class="loading-text">Connecting to HuggingFace...</div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <div class="settings-bar">
205
+ <button class="refresh-btn" onclick="fetchData()">REFRESH</button>
206
+ <span class="info">next in <span class="countdown" id="countdown">60</span>s</span>
207
+ <button class="refresh-btn" onclick="showTokenDialog()">TOKEN</button>
208
+ </div>
209
+
210
+ <!-- Token dialog -->
211
+ <div class="token-overlay" id="token-overlay" style="display:none">
212
+ <div class="token-box">
213
+ <h2>HuggingFace Token</h2>
214
+ <p>If your repo is private, enter your HF token. Leave empty for public repos.</p>
215
+ <input type="password" id="token-input" placeholder="hf_xxxxx" autocomplete="off">
216
+ <div class="btn-row">
217
+ <button class="btn-secondary" onclick="closeTokenDialog()">Cancel</button>
218
+ <button class="btn-primary" onclick="saveToken()">Save</button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <script>
224
+ const REPO = 'Wendy-Fly/Truck_2026';
225
+ const MONITOR_DIR = 'monitor';
226
+ const REFRESH_INTERVAL = 60;
227
+
228
+ let countdown = REFRESH_INTERVAL;
229
+ let timer = null;
230
+
231
+ // Clock
232
+ function updateClock() {
233
+ const now = new Date();
234
+ document.getElementById('clock').textContent =
235
+ now.toLocaleTimeString('en-GB', { hour12: false });
236
+ }
237
+ setInterval(updateClock, 1000);
238
+ updateClock();
239
+
240
+ // Token management
241
+ function getToken() { return localStorage.getItem('hf_token') || ''; }
242
+ function showTokenDialog() {
243
+ document.getElementById('token-overlay').style.display = 'flex';
244
+ document.getElementById('token-input').value = getToken();
245
+ }
246
+ function closeTokenDialog() { document.getElementById('token-overlay').style.display = 'none'; }
247
+ function saveToken() {
248
+ const t = document.getElementById('token-input').value.trim();
249
+ if (t) localStorage.setItem('hf_token', t);
250
+ else localStorage.removeItem('hf_token');
251
+ closeTokenDialog();
252
+ fetchData();
253
+ }
254
+
255
+ // Countdown
256
+ function startCountdown() {
257
+ countdown = REFRESH_INTERVAL;
258
+ if (timer) clearInterval(timer);
259
+ timer = setInterval(() => {
260
+ countdown--;
261
+ document.getElementById('countdown').textContent = countdown;
262
+ if (countdown <= 0) { fetchData(); }
263
+ }, 1000);
264
+ }
265
+
266
+ // Color by percentage
267
+ function pctColor(pct) {
268
+ if (pct >= 90) return 'red';
269
+ if (pct >= 70) return 'yellow';
270
+ return 'green';
271
+ }
272
+ function tempColor(t) {
273
+ if (t >= 85) return 'red';
274
+ if (t >= 70) return 'yellow';
275
+ return 'green';
276
+ }
277
+
278
+ // Format duration
279
+ function formatDuration(since) {
280
+ if (!since) return '--';
281
+ const ms = Date.now() - new Date(since).getTime();
282
+ const h = Math.floor(ms / 3600000);
283
+ const m = Math.floor((ms % 3600000) / 60000);
284
+ if (h > 24) return Math.floor(h / 24) + 'd ' + (h % 24) + 'h';
285
+ if (h > 0) return h + 'h ' + m + 'm';
286
+ return m + 'm';
287
+ }
288
+
289
+ // Format age
290
+ function formatAge(ts) {
291
+ if (!ts) return 'unknown';
292
+ const ms = Date.now() - new Date(ts).getTime();
293
+ const m = Math.floor(ms / 60000);
294
+ if (m < 1) return 'just now';
295
+ if (m < 60) return m + ' min ago';
296
+ const h = Math.floor(m / 60);
297
+ if (h < 24) return h + 'h ' + (m % 60) + 'm ago';
298
+ return Math.floor(h / 24) + 'd ago';
299
+ }
300
+
301
+ function isOffline(ts) {
302
+ if (!ts) return true;
303
+ return (Date.now() - new Date(ts).getTime()) > 15 * 60000;
304
+ }
305
+
306
+ // Render a server card
307
+ function renderServer(data) {
308
+ const hw = data.hardware || {};
309
+ const gpus = hw.gpus || [];
310
+ const tasks = data.tasks || [];
311
+ const offline = isOffline(data.timestamp);
312
+ const maxPct = Math.max(hw.cpu_percent || 0, hw.memory_percent || 0, hw.disk_percent || 0);
313
+ const cardClass = offline ? 'danger' : maxPct >= 90 ? 'danger' : maxPct >= 70 ? 'warn' : '';
314
+
315
+ let gpuHtml = '';
316
+ if (gpus.length) {
317
+ gpuHtml = `<div class="gpu-section"><div class="gpu-title">GPUs</div>`;
318
+ for (const g of gpus) {
319
+ const vramPct = g.memory_total_mb > 0 ? Math.round(g.memory_used_mb / g.memory_total_mb * 100) : 0;
320
+ const tc = tempColor(g.temp_c);
321
+ gpuHtml += `
322
+ <div class="gpu-card ${g.temp_c >= 85 ? 'hot' : ''}">
323
+ <div class="gpu-head">
324
+ <span class="gpu-name">#${g.id} ${g.name}</span>
325
+ <span class="gpu-temp c-${tc}">${g.temp_c}°C</span>
326
+ </div>
327
+ <div class="gpu-meters">
328
+ <div>
329
+ <div class="gpu-meter-label"><span>UTIL</span><span>${g.util_percent}%</span></div>
330
+ <div class="gpu-meter-bar"><div class="gpu-meter-fill bg-${pctColor(g.util_percent)}" style="width:${g.util_percent}%"></div></div>
331
+ </div>
332
+ <div>
333
+ <div class="gpu-meter-label"><span>VRAM</span><span>${vramPct}%</span></div>
334
+ <div class="gpu-meter-bar"><div class="gpu-meter-fill bg-${pctColor(vramPct)}" style="width:${vramPct}%"></div></div>
335
+ </div>
336
+ </div>
337
+ </div>`;
338
+ }
339
+ gpuHtml += `</div>`;
340
+ }
341
+
342
+ let tasksHtml = '';
343
+ if (tasks.length) {
344
+ tasksHtml = `<div class="tasks-section"><div class="tasks-title">Running Tasks (${tasks.length})</div>`;
345
+ for (const t of tasks) {
346
+ tasksHtml += `
347
+ <div class="task-item">
348
+ <span class="task-gpu-badge">GPU${t.gpu_id >= 0 ? t.gpu_id : '?'}</span>
349
+ <span class="task-cmd" title="${escHtml(t.command)}">${escHtml(t.command)}</span>
350
+ <span class="task-dur">${formatDuration(t.running_since)}</span>
351
+ </div>`;
352
+ }
353
+ tasksHtml += `</div>`;
354
+ }
355
+
356
+ return `
357
+ <div class="server-card ${cardClass}">
358
+ <div class="card-head">
359
+ <div class="card-host">
360
+ <span class="dot ${offline ? 'offline' : ''}"></span>
361
+ <h2>${escHtml(data.hostname)}</h2>
362
+ </div>
363
+ <span class="card-age">${formatAge(data.timestamp)}</span>
364
+ </div>
365
+ <div class="stats">
366
+ <div class="stat">
367
+ <div class="stat-label">CPU</div>
368
+ <div class="stat-value c-${pctColor(hw.cpu_percent || 0)}">${hw.cpu_percent || 0}<small>%</small></div>
369
+ <div class="stat-bar"><div class="stat-bar-fill bg-${pctColor(hw.cpu_percent || 0)}" style="width:${hw.cpu_percent || 0}%"></div></div>
370
+ </div>
371
+ <div class="stat">
372
+ <div class="stat-label">MEM</div>
373
+ <div class="stat-value c-${pctColor(hw.memory_percent || 0)}">${hw.memory_percent || 0}<small>%</small></div>
374
+ <div class="stat-bar"><div class="stat-bar-fill bg-${pctColor(hw.memory_percent || 0)}" style="width:${hw.memory_percent || 0}%"></div></div>
375
+ <div class="stat-sub">${hw.memory_used_gb || 0}/${hw.memory_total_gb || 0}G</div>
376
+ </div>
377
+ <div class="stat">
378
+ <div class="stat-label">DISK</div>
379
+ <div class="stat-value c-${pctColor(hw.disk_percent || 0)}">${hw.disk_percent || 0}<small>%</small></div>
380
+ <div class="stat-bar"><div class="stat-bar-fill bg-${pctColor(hw.disk_percent || 0)}" style="width:${hw.disk_percent || 0}%"></div></div>
381
+ <div class="stat-sub">${hw.disk_used_gb || 0}/${hw.disk_total_gb || 0}G</div>
382
+ </div>
383
+ </div>
384
+ ${gpuHtml}
385
+ ${tasksHtml}
386
+ </div>`;
387
+ }
388
+
389
+ function escHtml(s) {
390
+ const d = document.createElement('div');
391
+ d.textContent = s || '';
392
+ return d.innerHTML;
393
+ }
394
+
395
+ // Fetch data from HuggingFace
396
+ async function fetchData() {
397
+ countdown = REFRESH_INTERVAL;
398
+ const token = getToken();
399
+ const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
400
+ const errorEl = document.getElementById('error-container');
401
+ const listEl = document.getElementById('server-list');
402
+
403
+ try {
404
+ // List files in monitor/
405
+ const treeUrl = `https://huggingface.co/api/datasets/${REPO}/tree/main/${MONITOR_DIR}`;
406
+ const treeResp = await fetch(treeUrl, { headers });
407
+
408
+ if (!treeResp.ok) {
409
+ if (treeResp.status === 401 || treeResp.status === 403) {
410
+ errorEl.innerHTML = `<div class="error-banner">Access denied. Click TOKEN to enter your HF token.</div>`;
411
+ return;
412
+ }
413
+ if (treeResp.status === 404) {
414
+ listEl.innerHTML = `<div class="empty"><div class="empty-icon">~/</div><div class="empty-text">No monitor/ directory found yet.<br>Start a server agent to begin reporting.</div></div>`;
415
+ errorEl.innerHTML = '';
416
+ return;
417
+ }
418
+ throw new Error(`API ${treeResp.status}`);
419
+ }
420
+
421
+ const files = await treeResp.json();
422
+ const jsonFiles = files.filter(f => f.path.endsWith('.json'));
423
+
424
+ if (!jsonFiles.length) {
425
+ listEl.innerHTML = `<div class="empty"><div class="empty-icon">~/</div><div class="empty-text">No server data yet.<br>Waiting for first report...</div></div>`;
426
+ errorEl.innerHTML = '';
427
+ return;
428
+ }
429
+
430
+ // Fetch each JSON
431
+ const servers = [];
432
+ for (const f of jsonFiles) {
433
+ const url = `https://huggingface.co/datasets/${REPO}/resolve/main/${f.path}?t=${Date.now()}`;
434
+ const resp = await fetch(url, { headers });
435
+ if (resp.ok) {
436
+ servers.push(await resp.json());
437
+ }
438
+ }
439
+
440
+ // Sort: offline last, then by hostname
441
+ servers.sort((a, b) => {
442
+ const aOff = isOffline(a.timestamp);
443
+ const bOff = isOffline(b.timestamp);
444
+ if (aOff !== bOff) return aOff ? 1 : -1;
445
+ return (a.hostname || '').localeCompare(b.hostname || '');
446
+ });
447
+
448
+ listEl.innerHTML = servers.map(renderServer).join('');
449
+ errorEl.innerHTML = '';
450
+ document.getElementById('status-line').textContent = `LIVE · ${servers.length} server${servers.length > 1 ? 's' : ''}`;
451
+
452
+ } catch (err) {
453
+ errorEl.innerHTML = `<div class="error-banner">Fetch error: ${escHtml(err.message)}</div>`;
454
+ }
455
+
456
+ startCountdown();
457
+ }
458
+
459
+ // Init
460
+ fetchData();
461
+ </script>
462
+ </body>
463
+ </html>
manifest.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Server Monitor",
3
+ "short_name": "Monitor",
4
+ "description": "GPU Server Monitor Dashboard",
5
+ "start_url": ".",
6
+ "display": "standalone",
7
+ "background_color": "#0a0e17",
8
+ "theme_color": "#0a0e17",
9
+ "icons": [
10
+ {
11
+ "src": "icon.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ }
15
+ ]
16
+ }