Size
21.1 KB
Version
1.1.1
Created
Nov 27, 2025
Updated
15 days ago
1// ==UserScript==
2// @name SkyPilot Dashboard Logs Maximizer
3// @description Maximize logs view and show compact job details
4// @version 1.1.1
5// @match http://localhost/*
6// @icon http://localhost:46580/dashboard/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('SkyPilot Dashboard Logs Maximizer: Starting...');
12
13 // Debounce function for MutationObserver
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Initialize the extension
27 async function init() {
28 console.log('SkyPilot Dashboard Logs Maximizer: Initializing...');
29
30 // Wait for the logs and details sections to be available
31 const logsSection = await waitForElement('#logs');
32 const detailsSection = await waitForElement('#details');
33
34 if (!logsSection || !detailsSection) {
35 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs or details section');
36 return;
37 }
38
39 console.log('SkyPilot Dashboard Logs Maximizer: Found logs and details sections');
40
41 // Get saved state
42 const isMaximized = await GM.getValue('logsMaximized', false);
43 console.log('SkyPilot Dashboard Logs Maximizer: Saved state - maximized:', isMaximized);
44
45 // Add maximize button to logs header
46 addMaximizeButton(logsSection);
47
48 // Add auto-refresh button to logs header
49 addAutoRefreshButton(logsSection);
50
51 // Create compact details panel
52 createCompactDetailsPanel(detailsSection);
53
54 // Apply initial state
55 if (isMaximized) {
56 applyMaximizedState(logsSection, detailsSection);
57 }
58
59 // Check if auto-refresh was enabled
60 const autoRefreshEnabled = await GM.getValue('autoRefreshEnabled', false);
61 if (autoRefreshEnabled) {
62 startAutoRefresh();
63 // Update button state to show it's enabled
64 const autoRefreshBtn = document.getElementById('logs-autorefresh-btn');
65 if (autoRefreshBtn) {
66 updateAutoRefreshButtonState(autoRefreshBtn, true);
67 }
68 }
69
70 console.log('SkyPilot Dashboard Logs Maximizer: Initialization complete');
71 }
72
73 // Wait for element to appear in DOM
74 function waitForElement(selector, timeout = 10000) {
75 return new Promise((resolve) => {
76 const element = document.querySelector(selector);
77 if (element) {
78 resolve(element);
79 return;
80 }
81
82 const observer = new MutationObserver(debounce(() => {
83 const element = document.querySelector(selector);
84 if (element) {
85 observer.disconnect();
86 resolve(element);
87 }
88 }, 100));
89
90 observer.observe(document.body, {
91 childList: true,
92 subtree: true
93 });
94
95 setTimeout(() => {
96 observer.disconnect();
97 resolve(null);
98 }, timeout);
99 });
100 }
101
102 // Add maximize/minimize button to logs header
103 function addMaximizeButton(logsSection) {
104 const logsHeader = logsSection.querySelector('.flex.items-center.justify-between.px-4.pt-4');
105 if (!logsHeader) {
106 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs header');
107 return;
108 }
109
110 // Check if button already exists
111 if (document.getElementById('logs-maximize-btn')) {
112 return;
113 }
114
115 const buttonContainer = logsHeader.querySelector('.flex.items-center.space-x-3');
116 if (!buttonContainer) {
117 console.error('SkyPilot Dashboard Logs Maximizer: Could not find button container');
118 return;
119 }
120
121 const maximizeBtn = document.createElement('button');
122 maximizeBtn.id = 'logs-maximize-btn';
123 maximizeBtn.className = 'text-sky-blue hover:text-sky-blue-bright flex items-center';
124 maximizeBtn.innerHTML = `
125 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
126 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
127 </svg>
128 <span class="ml-1">Maximize</span>
129 `;
130 maximizeBtn.title = 'Maximize logs view';
131
132 maximizeBtn.addEventListener('click', async () => {
133 const detailsSection = document.querySelector('#details');
134 const isCurrentlyMaximized = await GM.getValue('logsMaximized', false);
135
136 if (isCurrentlyMaximized) {
137 applyMinimizedState(logsSection, detailsSection);
138 await GM.setValue('logsMaximized', false);
139 console.log('SkyPilot Dashboard Logs Maximizer: Minimized logs view');
140 } else {
141 applyMaximizedState(logsSection, detailsSection);
142 await GM.setValue('logsMaximized', true);
143 console.log('SkyPilot Dashboard Logs Maximizer: Maximized logs view');
144 }
145 });
146
147 buttonContainer.insertBefore(maximizeBtn, buttonContainer.firstChild);
148 console.log('SkyPilot Dashboard Logs Maximizer: Added maximize button');
149 }
150
151 // Add auto-refresh button to logs header
152 let autoRefreshInterval = null;
153
154 function addAutoRefreshButton(logsSection) {
155 const logsHeader = logsSection.querySelector('.flex.items-center.justify-between.px-4.pt-4');
156 if (!logsHeader) {
157 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs header');
158 return;
159 }
160
161 // Check if button already exists
162 if (document.getElementById('logs-autorefresh-btn')) {
163 return;
164 }
165
166 const buttonContainer = logsHeader.querySelector('.flex.items-center.space-x-3');
167 if (!buttonContainer) {
168 console.error('SkyPilot Dashboard Logs Maximizer: Could not find button container');
169 return;
170 }
171
172 const autoRefreshBtn = document.createElement('button');
173 autoRefreshBtn.id = 'logs-autorefresh-btn';
174 autoRefreshBtn.className = 'text-sky-blue hover:text-sky-blue-bright flex items-center';
175 autoRefreshBtn.innerHTML = `
176 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
177 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
178 </svg>
179 <span class="ml-1">Auto-Refresh: OFF</span>
180 `;
181 autoRefreshBtn.title = 'Toggle auto-refresh logs';
182
183 autoRefreshBtn.addEventListener('click', async () => {
184 const isEnabled = await GM.getValue('autoRefreshEnabled', false);
185
186 if (isEnabled) {
187 stopAutoRefresh();
188 await GM.setValue('autoRefreshEnabled', false);
189 updateAutoRefreshButtonState(autoRefreshBtn, false);
190 console.log('SkyPilot Dashboard Logs Maximizer: Auto-refresh disabled');
191 } else {
192 startAutoRefresh();
193 await GM.setValue('autoRefreshEnabled', true);
194 updateAutoRefreshButtonState(autoRefreshBtn, true);
195 console.log('SkyPilot Dashboard Logs Maximizer: Auto-refresh enabled');
196 }
197 });
198
199 // Insert after maximize button
200 const maximizeBtn = document.getElementById('logs-maximize-btn');
201 if (maximizeBtn) {
202 maximizeBtn.parentNode.insertBefore(autoRefreshBtn, maximizeBtn.nextSibling);
203 } else {
204 buttonContainer.insertBefore(autoRefreshBtn, buttonContainer.firstChild);
205 }
206
207 console.log('SkyPilot Dashboard Logs Maximizer: Added auto-refresh button');
208 }
209
210 // Update auto-refresh button visual state
211 function updateAutoRefreshButtonState(button, isEnabled) {
212 if (isEnabled) {
213 button.innerHTML = `
214 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
216 </svg>
217 <span class="ml-1">Auto-Refresh: ON</span>
218 `;
219 button.style.backgroundColor = '#0ea5e9';
220 button.style.color = 'white';
221 button.style.padding = '4px 8px';
222 button.style.borderRadius = '4px';
223 button.title = 'Auto-refresh is ON (click to disable)';
224 } else {
225 button.innerHTML = `
226 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
227 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
228 </svg>
229 <span class="ml-1">Auto-Refresh: OFF</span>
230 `;
231 button.style.backgroundColor = '';
232 button.style.color = '';
233 button.style.padding = '';
234 button.style.borderRadius = '';
235 button.title = 'Auto-refresh is OFF (click to enable)';
236 }
237 }
238
239 // Extract cluster name and job ID from URL
240 function getClusterAndJobInfo() {
241 const url = window.location.pathname;
242 const match = url.match(/\/dashboard\/clusters\/([^\/]+)\/(\d+)/);
243
244 if (match) {
245 return {
246 clusterName: match[1],
247 jobId: match[2]
248 };
249 }
250
251 return null;
252 }
253
254 // Fetch logs from API
255 async function fetchLogs() {
256 const info = getClusterAndJobInfo();
257
258 if (!info) {
259 console.error('SkyPilot Dashboard Logs Maximizer: Could not extract cluster name and job ID from URL');
260 return null;
261 }
262
263 console.log('SkyPilot Dashboard Logs Maximizer: Fetching logs for cluster:', info.clusterName, 'job:', info.jobId);
264
265 try {
266 const response = await GM.xmlhttpRequest({
267 method: 'POST',
268 url: 'http://localhost:46580/internal/dashboard/logs',
269 headers: {
270 'Content-Type': 'application/json',
271 'Accept': '*/*'
272 },
273 data: JSON.stringify({
274 follow: false,
275 cluster_name: info.clusterName,
276 job_id: info.jobId,
277 tail: 10000,
278 override_skypilot_config: {
279 active_workspace: 'default'
280 }
281 }),
282 responseType: 'json'
283 });
284
285 if (response.status === 200) {
286 console.log('SkyPilot Dashboard Logs Maximizer: Successfully fetched logs');
287 return response.response;
288 } else {
289 console.error('SkyPilot Dashboard Logs Maximizer: Failed to fetch logs, status:', response.status);
290 return null;
291 }
292 } catch (error) {
293 console.error('SkyPilot Dashboard Logs Maximizer: Error fetching logs:', error);
294 return null;
295 }
296 }
297
298 // Update logs display
299 function updateLogsDisplay(logsData) {
300 if (!logsData || !logsData.logs) {
301 console.error('SkyPilot Dashboard Logs Maximizer: Invalid logs data');
302 return;
303 }
304
305 const logsContainer = document.querySelector('#logs .logs-container');
306 if (!logsContainer) {
307 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs container');
308 return;
309 }
310
311 // Clear existing logs
312 logsContainer.innerHTML = '';
313
314 // Add new logs
315 Object.entries(logsData.logs).forEach(([node, logLines]) => {
316 logLines.forEach(line => {
317 const logLine = document.createElement('span');
318 logLine.className = 'log-line';
319
320 const message = document.createElement('span');
321 message.className = 'message';
322 message.textContent = line;
323
324 logLine.appendChild(message);
325 logsContainer.appendChild(logLine);
326 });
327 });
328
329 // Scroll to bottom after a short delay to ensure rendering is complete
330 setTimeout(() => {
331 const scrollContainer = document.querySelector('#logs .max-h-96.overflow-y-auto');
332 if (scrollContainer) {
333 scrollContainer.scrollTop = scrollContainer.scrollHeight;
334 console.log('SkyPilot Dashboard Logs Maximizer: Scrolled to bottom');
335 }
336 }, 100);
337
338 console.log('SkyPilot Dashboard Logs Maximizer: Updated logs display');
339 }
340
341 // Start auto-refresh
342 function startAutoRefresh() {
343 if (autoRefreshInterval) {
344 return; // Already running
345 }
346
347 // Fetch logs immediately
348 fetchLogs().then(logsData => {
349 if (logsData) {
350 updateLogsDisplay(logsData);
351 }
352 });
353
354 // Set up interval (refresh every 5 seconds)
355 autoRefreshInterval = setInterval(async () => {
356 const logsData = await fetchLogs();
357 if (logsData) {
358 updateLogsDisplay(logsData);
359 }
360 }, 5000);
361
362 // Update button appearance
363 const autoRefreshBtn = document.getElementById('logs-autorefresh-btn');
364 if (autoRefreshBtn) {
365 autoRefreshBtn.style.opacity = '0.6';
366 }
367
368 console.log('SkyPilot Dashboard Logs Maximizer: Auto-refresh started');
369 }
370
371 // Stop auto-refresh
372 function stopAutoRefresh() {
373 if (autoRefreshInterval) {
374 clearInterval(autoRefreshInterval);
375 autoRefreshInterval = null;
376 }
377
378 // Update button appearance
379 const autoRefreshBtn = document.getElementById('logs-autorefresh-btn');
380 if (autoRefreshBtn) {
381 autoRefreshBtn.style.opacity = '1';
382 }
383
384 console.log('SkyPilot Dashboard Logs Maximizer: Auto-refresh stopped');
385 }
386
387 // Create compact details panel
388 function createCompactDetailsPanel(detailsSection) {
389 // Check if compact panel already exists
390 if (document.getElementById('compact-details-panel')) {
391 return;
392 }
393
394 const compactPanel = document.createElement('div');
395 compactPanel.id = 'compact-details-panel';
396 compactPanel.style.cssText = `
397 display: none;
398 position: fixed;
399 top: 20px;
400 right: 20px;
401 background: white;
402 border: 1px solid #e5e7eb;
403 border-radius: 8px;
404 padding: 12px 16px;
405 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
406 z-index: 1000;
407 max-width: 300px;
408 font-size: 13px;
409 `;
410
411 // Extract relevant details
412 const detailsContent = detailsSection.querySelector('.grid.grid-cols-2.gap-6');
413 if (!detailsContent) {
414 console.error('SkyPilot Dashboard Logs Maximizer: Could not find details content');
415 return;
416 }
417
418 const details = {};
419 const detailItems = detailsContent.querySelectorAll('div > div');
420 for (let i = 0; i < detailItems.length; i += 2) {
421 const label = detailItems[i]?.textContent?.trim();
422 const value = detailItems[i + 1]?.textContent?.trim() || detailItems[i + 1]?.innerHTML?.trim();
423 if (label && value) {
424 details[label] = value;
425 }
426 }
427
428 // Build compact panel with only interesting details
429 let compactHTML = '<div style="font-weight: 600; margin-bottom: 8px; color: #1f2937;">Job Details</div>';
430
431 const interestingFields = ['Job ID', 'Status', 'Cluster'];
432 interestingFields.forEach(field => {
433 if (details[field]) {
434 let displayValue = details[field];
435
436 // Handle Status with badge
437 if (field === 'Status') {
438 const statusMatch = details[field].match(/SUCCEEDED|FAILED|RUNNING|PENDING/);
439 if (statusMatch) {
440 displayValue = statusMatch[0];
441 }
442 }
443
444 // Handle Cluster link
445 if (field === 'Cluster') {
446 const clusterLink = detailsContent.querySelector('a[href*="/dashboard/clusters/"]');
447 if (clusterLink) {
448 const clusterName = clusterLink.textContent.trim();
449 // Shorten cluster name if too long
450 displayValue = clusterName.length > 30 ? clusterName.substring(0, 30) + '...' : clusterName;
451 }
452 }
453
454 compactHTML += `
455 <div style="margin-bottom: 6px;">
456 <span style="color: #6b7280; font-weight: 500;">${field}:</span>
457 <span style="color: #1f2937; margin-left: 4px;">${displayValue}</span>
458 </div>
459 `;
460 }
461 });
462
463 compactPanel.innerHTML = compactHTML;
464 document.body.appendChild(compactPanel);
465 console.log('SkyPilot Dashboard Logs Maximizer: Created compact details panel');
466 }
467
468 // Apply maximized state
469 function applyMaximizedState(logsSection, detailsSection) {
470 console.log('SkyPilot Dashboard Logs Maximizer: Applying maximized state');
471
472 // Hide the full details section
473 if (detailsSection) {
474 detailsSection.style.display = 'none';
475 }
476
477 // Show compact details panel
478 const compactPanel = document.getElementById('compact-details-panel');
479 if (compactPanel) {
480 compactPanel.style.display = 'block';
481 }
482
483 // Maximize logs view - find the container more reliably
484 const logsContainer = logsSection.querySelector('div.max-h-96.overflow-y-auto');
485 if (logsContainer) {
486 logsContainer.style.maxHeight = 'calc(100vh - 250px)';
487 logsContainer.style.minHeight = '600px';
488 console.log('SkyPilot Dashboard Logs Maximizer: Applied maximize styles to logs container');
489 } else {
490 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs container');
491 }
492
493 // Update button text and icon
494 const maximizeBtn = document.getElementById('logs-maximize-btn');
495 if (maximizeBtn) {
496 maximizeBtn.innerHTML = `
497 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
498 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
499 </svg>
500 <span class="ml-1">Minimize</span>
501 `;
502 maximizeBtn.title = 'Minimize logs view';
503 }
504 }
505
506 // Apply minimized state
507 function applyMinimizedState(logsSection, detailsSection) {
508 console.log('SkyPilot Dashboard Logs Maximizer: Applying minimized state');
509
510 // Show the full details section
511 if (detailsSection) {
512 detailsSection.style.display = 'block';
513 }
514
515 // Hide compact details panel
516 const compactPanel = document.getElementById('compact-details-panel');
517 if (compactPanel) {
518 compactPanel.style.display = 'none';
519 }
520
521 // Restore logs view
522 const logsContainer = logsSection.querySelector('div.max-h-96.overflow-y-auto');
523 if (logsContainer) {
524 logsContainer.style.maxHeight = '24rem'; // 96 * 0.25rem = 24rem
525 logsContainer.style.minHeight = '';
526 console.log('SkyPilot Dashboard Logs Maximizer: Restored original logs container size');
527 } else {
528 console.error('SkyPilot Dashboard Logs Maximizer: Could not find logs container');
529 }
530
531 // Update button text and icon
532 const maximizeBtn = document.getElementById('logs-maximize-btn');
533 if (maximizeBtn) {
534 maximizeBtn.innerHTML = `
535 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
536 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
537 </svg>
538 <span class="ml-1">Maximize</span>
539 `;
540 maximizeBtn.title = 'Maximize logs view';
541 }
542 }
543
544 // Start the extension when DOM is ready
545 if (document.readyState === 'loading') {
546 document.addEventListener('DOMContentLoaded', init);
547 } else {
548 init();
549 }
550})();