SkyPilot Dashboard Logs Maximizer

Maximize logs view and show compact job details

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})();
SkyPilot Dashboard Logs Maximizer | Robomonkey