|
35 | 35 | data-bs-placement="top" |
36 | 36 | data-bs-html="true" |
37 | 37 | title="<div class='text-start small'> |
38 | | -<strong>Host:</strong> {{ gpustat.get('hostname', '-') }}<br> |
| 38 | +<strong>Node:</strong> {{ gpustat.get('hostname', '-') }}<br> |
39 | 39 | <strong>Device:</strong> {{ gpu.get('name', '-') }}<br> |
40 | 40 | <strong>Device ID:</strong> {{ gpu.get('index', '-') }}<br> |
41 | 41 | <strong>Memory:</strong> {{ gpu.get('memory.used', '-') }}/{{ gpu.get('memory.total', '-') }} MB<br> |
|
83 | 83 | <!-- GPU Stat Card--> |
84 | 84 | <div class="card mb-3"> |
85 | 85 | <div class="card-header"> |
86 | | - <i class="fas fa-table"></i> All Hosts and GPUs</div> |
| 86 | + <i class="fas fa-table"></i> All Nodes and GPUs</div> |
87 | 87 | <div class="card-body"> |
88 | 88 | <div class="table-responsive"> |
89 | 89 | <table class="table table-striped table-bordered" id="dataTable" width="100%"> |
90 | 90 | <thead> |
91 | 91 | <tr> |
92 | | - <th scope="col">Host</th> |
| 92 | + <th scope="col">Node</th> |
93 | 93 | <th scope="col">Device</th> |
94 | 94 | <th scope="col">Temp.</th> |
95 | 95 | <th scope="col">Util.</th> |
|
116 | 116 | </table> |
117 | 117 | </div> |
118 | 118 | </div> |
119 | | - <div class="card-footer small text-muted">{{ update_time }}</div> |
| 119 | + <div class="card-footer small text-muted px-3 py-2"> |
| 120 | + <div class="d-flex justify-content-between align-items-center"> |
| 121 | + <div id="update-timestamp">{{ update_time }}</div> |
| 122 | + <div class="d-flex align-items-center gap-3"> |
| 123 | + <span id="refresh-status">Auto-refresh: <span class="text-success">ON</span> (every 3s)</span> |
| 124 | + <a href="#" id="toggle-refresh" class="text-decoration-none small"> |
| 125 | + <i class="fas fa-pause me-1"></i>Pause |
| 126 | + </a> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + </div> |
| 130 | + </div> |
| 131 | + |
| 132 | + <div class="row mt-3"> |
| 133 | + <div class="col-12"> |
| 134 | + <div class="card bg-light"> |
| 135 | + <div class="card-body p-3"> |
| 136 | + <div class="d-flex align-items-center flex-wrap gap-3"> |
| 137 | + <h6 class="card-title mb-0 me-2"><i class="fas fa-palette me-2"></i>GPU Temperature Legend:</h6> |
| 138 | + <div class="d-flex flex-wrap gap-2"> |
| 139 | + <span class="badge rounded-pill bg-danger px-2 py-1">Hot (>75°C)</span> |
| 140 | + <span class="badge rounded-pill bg-warning px-2 py-1">Warm (50-75°C)</span> |
| 141 | + <span class="badge rounded-pill bg-success px-2 py-1">Normal (25-50°C)</span> |
| 142 | + <span class="badge rounded-pill bg-primary px-2 py-1">Cool (<25°C)</span> |
| 143 | + </div> |
| 144 | + </div> |
| 145 | + </div> |
| 146 | + </div> |
| 147 | + </div> |
120 | 148 | </div> |
| 149 | + |
121 | 150 | <footer class="py-4 bg-dark"> |
122 | 151 | <div class="container"> |
123 | 152 | <div class="text-center"> |
|
135 | 164 | <script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js"></script> |
136 | 165 | <script src="https://cdn.datatables.net/2.0.3/js/dataTables.bootstrap5.min.js"></script> |
137 | 166 | <script> |
| 167 | + let dataTable; |
| 168 | + let refreshInterval; |
| 169 | + let isRefreshEnabled = true; |
| 170 | + |
138 | 171 | document.addEventListener('DOMContentLoaded', function() { |
139 | | - new DataTable('#dataTable'); |
| 172 | + dataTable = new DataTable('#dataTable', { |
| 173 | + createdRow: function(row, data, dataIndex) { |
| 174 | + row.classList.add('small'); |
| 175 | + }, |
| 176 | + columnDefs: [ |
| 177 | + { className: 'text-start', targets: '_all' } |
| 178 | + ] |
| 179 | + }); |
140 | 180 |
|
141 | 181 | var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); |
142 | 182 | var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { |
143 | 183 | return new bootstrap.Tooltip(tooltipTriggerEl); |
144 | 184 | }); |
| 185 | + |
| 186 | + const toggleButton = document.getElementById('toggle-refresh'); |
| 187 | + const refreshStatus = document.getElementById('refresh-status'); |
| 188 | + |
| 189 | + toggleButton.addEventListener('click', function(e) { |
| 190 | + e.preventDefault(); |
| 191 | + isRefreshEnabled = !isRefreshEnabled; |
| 192 | + |
| 193 | + if (isRefreshEnabled) { |
| 194 | + refreshInterval = setInterval(updateDashboard, 3000); |
| 195 | + toggleButton.innerHTML = '<i class="fas fa-pause me-1"></i>Pause'; |
| 196 | + refreshStatus.innerHTML = 'Auto-refresh: <span class="text-success">ON</span> (every 3s)'; |
| 197 | + } else { |
| 198 | + clearInterval(refreshInterval); |
| 199 | + toggleButton.innerHTML = '<i class="fas fa-play me-1"></i>Resume'; |
| 200 | + refreshStatus.innerHTML = 'Auto-refresh: <span class="text-danger">PAUSED</span>'; |
| 201 | + } |
| 202 | + }); |
| 203 | + |
| 204 | + // Auto-refresh every 3 seconds |
| 205 | + refreshInterval = setInterval(updateDashboard, 3000); |
145 | 206 | }); |
| 207 | + |
| 208 | + async function updateDashboard() { |
| 209 | + try { |
| 210 | + const response = await fetch('/api/gpustat/all'); |
| 211 | + if (!response.ok) return; |
| 212 | + |
| 213 | + const gpustats = await response.json(); |
| 214 | + updateCards(gpustats); |
| 215 | + updateTable(gpustats); |
| 216 | + const now = new Date(); |
| 217 | + const timeString = now.toLocaleString() + ' ' + Intl.DateTimeFormat().resolvedOptions().timeZone; |
| 218 | + document.getElementById('update-timestamp').textContent = `Updated at ${timeString}`; |
| 219 | + } catch (error) { |
| 220 | + console.log('Auto-refresh failed:', error); |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + function updateCards(gpustats) { |
| 225 | + var existingTooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); |
| 226 | + existingTooltips.forEach(function(element) { |
| 227 | + var tooltip = bootstrap.Tooltip.getInstance(element); |
| 228 | + if (tooltip) { |
| 229 | + tooltip.dispose(); |
| 230 | + } |
| 231 | + }); |
| 232 | + |
| 233 | + const cardsContainer = document.querySelector('.row'); |
| 234 | + cardsContainer.innerHTML = ''; |
| 235 | + |
| 236 | + gpustats.forEach(gpustat => { |
| 237 | + gpustat.gpus.forEach(gpu => { |
| 238 | + const temp = gpu['temperature.gpu']; |
| 239 | + let flag = 'bg-primary'; |
| 240 | + if (temp > 75) flag = 'bg-danger'; |
| 241 | + else if (temp > 50) flag = 'bg-warning'; |
| 242 | + else if (temp > 25) flag = 'bg-success'; |
| 243 | + |
| 244 | + const cardHtml = ` |
| 245 | + <div class="col-xl-3 col-md-4 col-sm-6 mb-3"> |
| 246 | + <div class="card text-white ${flag} h-100" |
| 247 | + data-bs-toggle="tooltip" |
| 248 | + data-bs-placement="top" |
| 249 | + data-bs-html="true" |
| 250 | + title="<div class='text-start small'> |
| 251 | +<strong>Node:</strong> ${gpustat.hostname || '-'}<br> |
| 252 | +<strong>Device:</strong> ${gpu.name || '-'}, ID: ${gpu.index || '-'}<br> |
| 253 | +<strong>Memory:</strong> ${gpu['memory.used'] || '-'} / ${gpu['memory.total'] || '-'} MB<br> |
| 254 | +<strong>Power:</strong> ${gpu['power.draw'] || '-'} / ${gpu['enforced.power.limit'] || '-'} W<br> |
| 255 | +<strong>Temperature:</strong> ${gpu['temperature.gpu'] || '-'}°C<br> |
| 256 | +<strong>Utilization:</strong> ${gpu['utilization.gpu'] || '-'}%<br> |
| 257 | +<strong>Processes:</strong> ${gpu['user_processes'] || '-'} |
| 258 | +</div>"> |
| 259 | + <div class="card-body"> |
| 260 | + <div class="d-flex align-items-center"> |
| 261 | + <div> |
| 262 | + <i class="fas fa-server me-2"></i> |
| 263 | + <b>${gpustat.hostname || '-'}</b> |
| 264 | + </div> |
| 265 | + </div> |
| 266 | + <div class="mt-2"> |
| 267 | + [${gpu.index || ''}] ${gpu.name || '-'} |
| 268 | + </div> |
| 269 | + </div> |
| 270 | + <div class="card-footer text-white small"> |
| 271 | + <div class="row g-1"> |
| 272 | + <div class="col-3"> |
| 273 | + <i class="fas fa-temperature-three-quarters" aria-hidden="true"></i> |
| 274 | + ${gpu['temperature.gpu'] || '-'}°C |
| 275 | + </div> |
| 276 | + <div class="col-3"> |
| 277 | + <i class="fas fa-memory" aria-hidden="true"></i> |
| 278 | + ${gpu.memory || '-'}% |
| 279 | + </div> |
| 280 | + <div class="col-3"> |
| 281 | + <i class="fas fa-cogs" aria-hidden="true"></i> |
| 282 | + ${gpu['utilization.gpu'] || '-'}% |
| 283 | + </div> |
| 284 | + <div class="col-3"> |
| 285 | + <i class="fas fa-users" aria-hidden="true"></i> |
| 286 | + ${gpu.users || '-'} |
| 287 | + </div> |
| 288 | + </div> |
| 289 | + </div> |
| 290 | + </div> |
| 291 | + </div> |
| 292 | + `; |
| 293 | + cardsContainer.insertAdjacentHTML('beforeend', cardHtml); |
| 294 | + }); |
| 295 | + }); |
| 296 | + |
| 297 | + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); |
| 298 | + var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { |
| 299 | + return new bootstrap.Tooltip(tooltipTriggerEl); |
| 300 | + }); |
| 301 | + } |
| 302 | + |
| 303 | + function updateTable(gpustats) { |
| 304 | + dataTable.clear(); |
| 305 | + const newData = []; |
| 306 | + gpustats.forEach(gpustat => { |
| 307 | + gpustat.gpus.forEach(gpu => { |
| 308 | + newData.push([ |
| 309 | + gpustat.hostname || '-', |
| 310 | + `[${gpu.index || ''}] ${gpu.name || '-'}`, |
| 311 | + `${gpu['temperature.gpu'] || '-'}°C`, |
| 312 | + `${gpu['utilization.gpu'] || '-'}%`, |
| 313 | + `${gpu.memory || '-'}% (${gpu['memory.used'] || ''}/${gpu['memory.total'] || '-'})`, |
| 314 | + `${gpu['power.draw'] || '-'} / ${gpu['enforced.power.limit'] || '-'}`, |
| 315 | + gpu['user_processes'] || '-' |
| 316 | + ]); |
| 317 | + }); |
| 318 | + }); |
| 319 | + dataTable.rows.add(newData); |
| 320 | + dataTable.draw(false); |
| 321 | + } |
146 | 322 | </script> |
147 | 323 | </div> |
148 | 324 | </body> |
|
0 commit comments