|
96 | 96 | } |
97 | 97 |
|
98 | 98 | const panelHTML = ` |
99 | | - <div id="cm-global-floating-panel" style="position: fixed; bottom: 20px; right: 20px; z-index: 1050; width: 320px;"> |
100 | | - <div class="card shadow-lg" style="border: 2px solid #479abf;"> |
101 | | - <div class="card-header text-white" style="cursor: pointer; background-color: #479abf; padding: 0.75rem 1rem;" onclick="window.cmGlobalPanel.toggle()"> |
102 | | - <div style="display: flex; align-items: center; justify-content: space-between;"> |
103 | | - <div style="display: flex; align-items: center; gap: 8px;"> |
104 | | - <i class="fas fa-server"></i> |
105 | | - <span style="font-weight: 600; font-size: 0.95rem;">Lab Instance</span> |
| 99 | + <div id="cm-global-floating-panel" style="position: fixed; bottom: 20px; right: 20px; z-index: 1050; width: 340px;"> |
| 100 | + <div class="card shadow-lg" style="background-color: #1e1e1e; border: 1px solid #333; border-radius: 10px; overflow: hidden; box-shadow: 0 8px 16px rgba(0,0,0,0.5) !important;"> |
| 101 | + <div class="card-header" style="background-color: #161616; border-bottom: 1px solid #2a2a2a; color: #fff; cursor: pointer; padding: 12px 16px;" onclick="window.cmGlobalPanel.toggle()"> |
| 102 | + <div class="d-flex justify-content-between align-items-center"> |
| 103 | + <div class="d-flex align-items-center" style="font-weight: 700; font-size: 15px;"> |
| 104 | + <i class="fas fa-server mr-2 text-primary"></i> |
| 105 | + Lab Instance |
106 | 106 | </div> |
107 | | - <div style="display: flex; align-items: center; gap: 8px;"> |
108 | | - <span id="cm-global-header-time" style="font-size: 0.9rem; font-weight: 500;"></span> |
109 | | - <i id="cm-global-toggle-icon" class="fas fa-chevron-up"></i> |
110 | | - <button type="button" class="close text-white" style="padding: 0; margin: 0;" onclick="window.cmGlobalPanel.close(event)"> |
111 | | - <span>×</span> |
| 107 | + <div class="d-flex align-items-center"> |
| 108 | + <span id="cm-global-header-time" class="mr-3 d-none" style="font-family: monospace; font-weight: bold; color: #ebb850; font-size: 14px;"></span> |
| 109 | + <i id="cm-global-toggle-icon" class="fas fa-chevron-up mr-2" style="color: #888;"></i> |
| 110 | + <button type="button" class="close ml-1" onclick="window.cmGlobalPanel.close(event)" aria-label="Close" style="color: #aaa; text-shadow: none; opacity: 1;"> |
| 111 | + <span aria-hidden="true">×</span> |
112 | 112 | </button> |
113 | 113 | </div> |
114 | 114 | </div> |
115 | 115 | </div> |
116 | | - <div id="cm-global-panel-body" class="card-body"> |
117 | | - <div id="cm-global-loading" style="display: none;"> |
118 | | - <p class="text-center text-muted"> |
| 116 | + <div id="cm-global-panel-body" class="card-body" style="padding: 16px; color: #efefef;"> |
| 117 | + <div id="cm-global-loading" class="d-none"> |
| 118 | + <p class="text-center mb-0" style="color: #888;"> |
119 | 119 | <i class="fas fa-spinner fa-spin"></i> Loading... |
120 | 120 | </p> |
121 | 121 | </div> |
122 | 122 | |
123 | | - <div id="cm-global-content"> |
124 | | - </div> |
| 123 | + <div id="cm-global-content"></div> |
125 | 124 | </div> |
126 | 125 | </div> |
127 | 126 | </div> |
128 | 127 | |
129 | 128 | <style> |
130 | | - #cm-global-floating-panel .card { |
131 | | - border-width: 2px; |
132 | | - } |
133 | | - #cm-global-floating-panel .card-header { |
134 | | - padding: 0.5rem 1rem; |
135 | | - } |
136 | | - #cm-global-floating-panel .card-body { |
137 | | - padding: 1rem; |
138 | | - } |
139 | 129 | #cm-global-floating-panel.minimized #cm-global-panel-body { |
140 | 130 | display: none; |
141 | 131 | } |
| 132 | + .floating-btn-lab { |
| 133 | + border: none; |
| 134 | + border-radius: 6px; |
| 135 | + font-weight: 700; |
| 136 | + transition: transform 0.1s, opacity 0.2s; |
| 137 | + cursor: pointer; |
| 138 | + display: flex; |
| 139 | + align-items: center; |
| 140 | + justify-content: center; |
| 141 | + gap: 6px; |
| 142 | + padding: 8px 10px; |
| 143 | + } |
| 144 | + .floating-btn-lab:hover { opacity: 0.9; transform: translateY(-1px); } |
| 145 | + .floating-btn-lab:active { transform: translateY(0); } |
142 | 146 | @media (max-width: 768px) { |
143 | 147 | #cm-global-floating-panel { |
144 | | - width: calc(100% - 40px); |
145 | | - bottom: 10px; |
146 | | - right: 10px; |
| 148 | + width: calc(100% - 40px) !important; |
| 149 | + bottom: 10px !important; |
| 150 | + right: 10px !important; |
147 | 151 | } |
148 | 152 | } |
149 | 153 | </style> |
|
157 | 161 | const contentDiv = document.getElementById('cm-global-content'); |
158 | 162 | if (!contentDiv) return; |
159 | 163 |
|
160 | | - let { challengeId, connectionInfo, until, since, category, created_at, extra_time } = instanceData; |
161 | | - |
162 | | - // terraform-challenge-manager doesn't return 'until', calculate it from created_at + extra_time |
163 | | - if (!until && created_at && extra_time !== undefined) { |
164 | | - const createdDate = new Date(created_at); |
165 | | - const timeout = parseInt(extra_time) || 7200; // Default 2 hours |
166 | | - until = new Date(createdDate.getTime() + timeout * 1000).toISOString(); |
167 | | - // Update instanceData for storage |
168 | | - instanceData.until = until; |
169 | | - setActiveInstance(instanceData); |
170 | | - } |
| 164 | + let { challengeId, connectionInfo, until, since, category, challengeName } = instanceData; |
171 | 165 |
|
172 | 166 | // Calculate time remaining |
173 | 167 | let countdownHTML = ''; |
|
182 | 176 | if (diff > 0) { |
183 | 177 | timeLeft = diff; |
184 | 178 | countdownHTML = ` |
185 | | - <p class="mb-1"> |
186 | | - <strong>Time Left:</strong> |
187 | | - <span id="cm-global-countdown" class="badge badge-info">${formatCountdown(diff)}</span> |
188 | | - </p> |
| 179 | + <div class="mb-3"> |
| 180 | + <strong class="d-block mb-1" style="color: #888; font-size: 12px; text-transform: uppercase;">Time Remaining:</strong> |
| 181 | + <div id="cm-global-countdown" style="font-family: monospace; font-size: 1.2rem; font-weight: bold; color: #fff;">${formatCountdown(diff)}</div> |
| 182 | + </div> |
189 | 183 | `; |
190 | 184 |
|
191 | 185 | // Check for expiration warnings |
|
218 | 212 |
|
219 | 213 | if (diff <= FIFTEEN_MIN) { |
220 | 214 | warningHTML = ` |
221 | | - <div class="alert alert-warning alert-sm p-2 mb-2" role="alert" style="font-size: 0.85rem;"> |
222 | | - <i class="fas fa-exclamation-triangle"></i> ${warningMessage} |
| 215 | + <div class="alert alert-warning py-2 px-2 mb-3 small" role="alert" style="background-color: rgba(235, 184, 80, 0.1); border: 1px solid #ebb850; color: #ebb850;"> |
| 216 | + <i class="fas fa-exclamation-triangle mr-1"></i> ${warningMessage} |
223 | 217 | </div> |
224 | 218 | `; |
225 | 219 | } |
|
230 | 224 | const headerTimeEl = document.getElementById('cm-global-header-time'); |
231 | 225 | if (headerTimeEl && timeLeft) { |
232 | 226 | headerTimeEl.textContent = formatCountdown(timeLeft); |
233 | | - headerTimeEl.style.display = 'inline'; |
| 227 | + headerTimeEl.classList.remove('d-none'); |
234 | 228 | } else if (headerTimeEl) { |
235 | | - headerTimeEl.style.display = 'none'; |
| 229 | + headerTimeEl.classList.add('d-none'); |
| 230 | + } |
| 231 | + |
| 232 | + let categoryHTML = ''; |
| 233 | + if (challengeName) { |
| 234 | + categoryHTML = ` |
| 235 | + <div class="mb-3"> |
| 236 | + <strong class="d-block mb-1" style="color: #888; font-size: 12px; text-transform: uppercase;">Lab:</strong> |
| 237 | + <div style="font-size: 13px; font-weight: 500; color: #fff; word-wrap: break-word; overflow-wrap: break-word; white-space: normal; line-height: 1.4;"> |
| 238 | + ${challengeName} |
| 239 | + </div> |
| 240 | + </div> |
| 241 | + `; |
236 | 242 | } |
237 | 243 |
|
238 | 244 | const html = ` |
| 245 | + ${categoryHTML} |
239 | 246 | ${warningHTML} |
240 | 247 | ${countdownHTML} |
241 | 248 | |
242 | | - <div class="mb-3"> |
243 | | - <small class="text-muted font-weight-bold">Connection Info:</small> |
244 | | - <div class="p-2 bg-light rounded" style="font-family: monospace; font-size: 0.85rem; word-break: break-all; border: 1px solid #ddd;"> |
245 | | - ${connectionInfo ? connectionInfo.replace(/\n/g, '<br>') : 'Waiting for connection details...'} |
246 | | - </div> |
247 | | - </div> |
248 | | -
|
249 | | - <div class="btn-group btn-group-sm d-flex" role="group"> |
250 | | - <button type="button" class="btn btn-warning flex-fill" onclick="window.cmGlobalPanel.renew()" title="Add more time"> |
| 249 | + <div class="d-flex mt-3" style="gap: 10px;"> |
| 250 | + <button type="button" class="flex-fill floating-btn-lab" style="background-color:#ebb850; color: #000;" onclick="window.cmGlobalPanel.renew()" title="Add more time"> |
251 | 251 | <i class="fas fa-clock"></i> Renew |
252 | 252 | </button> |
253 | | - <button type="button" class="btn btn-info flex-fill" onclick="window.cmGlobalPanel.restart()" title="Restart instance"> |
| 253 | + <button type="button" class="flex-fill floating-btn-lab" style="background-color:#479abf; color: #fff;" onclick="window.cmGlobalPanel.restart()" title="Restart instance"> |
254 | 254 | <i class="fas fa-redo"></i> Restart |
255 | 255 | </button> |
256 | | - <button type="button" class="btn btn-danger flex-fill" onclick="window.cmGlobalPanel.destroy()" title="Destroy instance"> |
| 256 | + <button type="button" class="flex-fill floating-btn-lab" style="background-color:#b43c2c; color: #fff;" onclick="window.cmGlobalPanel.destroy()" title="Destroy instance"> |
257 | 257 | <i class="fas fa-trash"></i> Destroy |
258 | 258 | </button> |
259 | 259 | </div> |
|
461 | 461 | if (data.success) { |
462 | 462 | // Reset warning flags |
463 | 463 | setWarningsShown({ fifteen: false, ten: false, five: false }); |
464 | | - // Refresh instance data |
| 464 | + |
| 465 | + // If response includes updated instance data, use it immediately |
| 466 | + if (data.data && data.data.until) { |
| 467 | + const activeInstance = getActiveInstance(); |
| 468 | + if (activeInstance) { |
| 469 | + activeInstance.until = data.data.until; |
| 470 | + if (data.data.connectionInfo) { |
| 471 | + activeInstance.connectionInfo = data.data.connectionInfo; |
| 472 | + } |
| 473 | + setActiveInstance(activeInstance); |
| 474 | + updatePanelContent(activeInstance); |
| 475 | + } |
| 476 | + } |
| 477 | + |
| 478 | + // Also poll to ensure we're in sync |
465 | 479 | pollInstanceStatus(); |
| 480 | + |
466 | 481 | if (window.CTFd && window.CTFd._functions && window.CTFd._functions.events) { |
467 | 482 | CTFd._functions.events.eventAlert({ |
468 | 483 | title: "Success", |
|
537 | 552 | .then(response => response.json()) |
538 | 553 | .then(data => { |
539 | 554 | if (data.success) { |
| 555 | + // Update immediately if response includes instance data |
| 556 | + if (data.data) { |
| 557 | + const activeInstance = getActiveInstance(); |
| 558 | + if (activeInstance) { |
| 559 | + Object.assign(activeInstance, data.data); |
| 560 | + setActiveInstance(activeInstance); |
| 561 | + updatePanelContent(activeInstance); |
| 562 | + } |
| 563 | + } |
| 564 | + |
| 565 | + // Also poll to ensure we're in sync |
540 | 566 | pollInstanceStatus(); |
| 567 | + |
541 | 568 | if (window.CTFd && window.CTFd._functions && window.CTFd._functions.events) { |
542 | 569 | CTFd._functions.events.eventAlert({ |
543 | 570 | title: "Success", |
|
565 | 592 | }, |
566 | 593 |
|
567 | 594 | destroy: function() { |
568 | | - if (!confirm('Are you sure you want to destroy your instance? All progress will be lost.')) { |
| 595 | + if (!confirm('Are you sure you want to destroy your instance? Your Azure lab environment will be destroyed.')) { |
569 | 596 | return; |
570 | 597 | } |
571 | 598 |
|
|
627 | 654 | const data = { |
628 | 655 | challengeId: challengeId, |
629 | 656 | category: category, |
| 657 | + challengeName: instanceData.challengeName || 'Lab Instance', |
630 | 658 | connectionInfo: instanceData.connectionInfo, |
631 | 659 | until: instanceData.until, |
632 | 660 | since: instanceData.since |
|
0 commit comments