Umbraco Cloud Deployment Viewer

Comprehensive deployment monitoring interface for Umbraco Cloud with real-time logs and status tracking

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Umbraco Cloud Deployment Viewer
// @namespace    https://github.com/skttl/umbraco-userscripts
// @version      1.0.0
// @description  Comprehensive deployment monitoring interface for Umbraco Cloud with real-time logs and status tracking
// @author       skttl
// @homepage     https://github.com/skttl/umbraco-userscripts
// @supportURL   https://github.com/skttl/umbraco-userscripts/issues
// @license      MIT
// @include      /^https?:\/\/.*\.scm\..*\.umbraco\.io\/.*$/
// @icon         https://raw.githubusercontent.com/skttl/umbraco-userscripts/main/screenshots/deployment_status.png
// @grant        none
// @run-at       document-end
// @compatible   chrome Tampermonkey
// @compatible   firefox Tampermonkey
// @compatible   edge Tampermonkey
// ==/UserScript==

(function() {
    'use strict';

    let isViewerActive = false;
    let logRefreshInterval = null;
    let currentDeploymentId = null;

    function addNavbarLink() {
        const navbar = document.querySelector('body > .navbar:first-child');
        if (!navbar) return;

        const navList = navbar.querySelector('.nav.navbar-nav');
        if (!navList || document.getElementById('deployment-nav-link')) return;

        const li = document.createElement('li');
        li.id = 'deployment-nav-link';

        const link = document.createElement('a');
        link.href = '#';
        link.textContent = 'Deployments';
        link.onclick = (e) => {
            e.preventDefault();
            toggleDeploymentViewer();
        };

        li.appendChild(link);
        navList.appendChild(li);
    }

    function toggleDeploymentViewer(skipHistory = false) {
        if (isViewerActive) {
            hideDeploymentViewer(skipHistory);
        } else {
            showDeploymentViewer(skipHistory);
        }
    }

    function showDeploymentViewer(skipHistory = false) {
        const navbar = document.querySelector('body > .navbar:first-child');
        if (!navbar) return;

        const navList = navbar.querySelector('.nav.navbar-nav');
        if (navList) {
            navList.querySelectorAll('li').forEach(li => li.classList.remove('active'));
        }

        window.dispatchEvent(new CustomEvent('viewer-change', { detail: { viewer: 'deployments' } }));

        const eventlogPanel = document.getElementById('eventlog-viewer-panel');
        if (eventlogPanel) {
            eventlogPanel.style.display = 'none';
        }

        let nextSibling = navbar.nextElementSibling;
        while (nextSibling) {
            if (nextSibling.id !== 'deployment-viewer-panel' && nextSibling.id !== 'eventlog-viewer-panel') {
                nextSibling.style.display = 'none';
            }
            nextSibling = nextSibling.nextElementSibling;
        }

        if (!document.getElementById('deployment-viewer-panel')) {
            createViewerPanel();
            fetchDeploymentData();
        } else {
            document.getElementById('deployment-viewer-panel').style.display = 'block';
        }

        isViewerActive = true;

        if (!skipHistory) {
            history.pushState({ view: 'deployments' }, 'Deployments', window.location.pathname + '?deployments');
        }
        
        updateNavbarState();
    }

    function hideDeploymentViewer(skipHistory = false) {
        const navbar = document.querySelector('body > .navbar:first-child');
        if (!navbar) return;

        stopLogRefresh();

        let nextSibling = navbar.nextElementSibling;
        while (nextSibling) {
            if (nextSibling.id !== 'deployment-viewer-panel') {
                nextSibling.style.display = '';
            }
            nextSibling = nextSibling.nextElementSibling;
        }

        const panel = document.getElementById('deployment-viewer-panel');
        if (panel) {
            panel.style.display = 'none';
        }

        isViewerActive = false;

        if (!skipHistory) {
            history.pushState({ view: null }, '', window.location.pathname);
        }
        
        updateNavbarState();
    }

    function createViewerPanel() {
        const panel = document.createElement('div');
        panel.id = 'deployment-viewer-panel';
        panel.className = 'container';
        panel.style.marginTop = '20px';

        const header = document.createElement('div');
        header.className = 'page-header';
        header.innerHTML = '<h1>Deployment Status</h1>';
        panel.appendChild(header);

        const btnGroup = document.createElement('div');
        btnGroup.className = 'btn-group';
        btnGroup.style.marginBottom = '20px';

        const refreshBtn = document.createElement('button');
        refreshBtn.className = 'btn btn-primary';
        refreshBtn.textContent = 'Refresh';
        refreshBtn.onclick = fetchDeploymentData;
        btnGroup.appendChild(refreshBtn);

        const triggerBtn = document.createElement('button');
        triggerBtn.className = 'btn btn-success';
        triggerBtn.textContent = 'Trigger New Deployment';
        triggerBtn.style.marginLeft = '10px';
        triggerBtn.onclick = triggerNewDeployment;
        btnGroup.appendChild(triggerBtn);

        panel.appendChild(btnGroup);

        const content = document.createElement('div');
        content.id = 'deployment-content';
        panel.appendChild(content);

        document.body.appendChild(panel);
    }

    function getStatusBadge(status) {
        const statusMap = {
            'Pending': { class: 'default' },
            'Building': { class: 'info' },
            'Deploying': { class: 'info' },
            'Failed': { class: 'danger' },
            'Success': { class: 'success' },
            'Loading': { class: 'default' }
        };
        const info = statusMap[status] || { class: 'default' };
        return `<span class="label label-${info.class}">${status}</span>`;
    }

    function getStatusText(numericStatus) {
        const statusMap = {
            0: 'Pending',
            1: 'Building',
            2: 'Deploying',
            3: 'Failed',
            4: 'Success'
        };
        return statusMap[numericStatus] || 'Unknown';
    }

    async function fetchDeploymentStatus(id) {
        try {
            const res = await fetch(`/api/vfs/site/deployments/${id}/status.xml`);
            if (!res.ok) return null;
            const text = await res.text();
            const parser = new DOMParser();
            const xml = parser.parseFromString(text, 'application/xml');
            
            return {
                id: xml.querySelector('id')?.textContent || id,
                status: xml.querySelector('status')?.textContent || 'Unknown',
                author: xml.querySelector('author')?.textContent || '',
                authorEmail: xml.querySelector('authorEmail')?.textContent || '',
                message: xml.querySelector('message')?.textContent || '',
                deployer: xml.querySelector('deployer')?.textContent || '',
                receivedTime: xml.querySelector('receivedTime')?.textContent || '',
                startTime: xml.querySelector('startTime')?.textContent || '',
                endTime: xml.querySelector('endTime')?.textContent || '',
                lastSuccessEndTime: xml.querySelector('lastSuccessEndTime')?.textContent || '',
                complete: xml.querySelector('complete')?.textContent || '',
                projectType: xml.querySelector('project_type')?.textContent || ''
            };
        } catch (err) {
            return null;
        }
    }

    function showDeploymentDetails(deployment, activeId) {
        const modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '0';
        modal.style.left = '0';
        modal.style.width = '100%';
        modal.style.height = '100%';
        modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
        modal.style.zIndex = '10000';
        modal.style.display = 'flex';
        modal.style.alignItems = 'center';
        modal.style.justifyContent = 'center';
        modal.onclick = (e) => {
            if (e.target === modal) modal.remove();
        };

        const isActive = deployment.id === activeId;
        const duration = deployment.startTime && deployment.endTime 
            ? formatDuration(deployment.startTime, deployment.endTime)
            : 'N/A';

        const dialog = document.createElement('div');
        dialog.className = 'panel panel-default';
        dialog.style.width = '80%';
        dialog.style.maxWidth = '800px';
        dialog.style.maxHeight = '90%';
        dialog.style.overflow = 'auto';
        dialog.style.margin = '0';

        dialog.innerHTML = `
            <div class="panel-heading">
                <h3 class="panel-title">
                    Deployment Details
                    ${isActive ? '<span class="label label-warning" style="margin-left: 10px;">ACTIVE</span>' : ''}
                    <button type="button" class="close" style="margin-top: -2px;">&times;</button>
                </h3>
            </div>
            <div class="panel-body">
                <div class="row">
                    <div class="col-md-6">
                        <p><strong>Status:</strong> ${getStatusBadge(deployment.status)}</p>
                        <p><strong>ID:</strong> <code>${deployment.id}</code></p>
                        <p><strong>Author:</strong> ${deployment.author} (${deployment.authorEmail})</p>
                        <p><strong>Message:</strong> ${deployment.message}</p>
                        <p><strong>Complete:</strong> ${deployment.complete}</p>
                        <p id="modal-files-count-${deployment.id}"><strong>Files:</strong> <span style="color: #999;">Loading...</span></p>
                    </div>
                    <div class="col-md-6">
                        <p><strong>Received:</strong> ${deployment.receivedTime ? new Date(deployment.receivedTime).toLocaleString() : 'N/A'}</p>
                        <p><strong>Started:</strong> ${deployment.startTime ? new Date(deployment.startTime).toLocaleString() : 'N/A'}</p>
                        <p><strong>Completed:</strong> ${deployment.endTime ? new Date(deployment.endTime).toLocaleString() : 'N/A'}</p>
                        <p><strong>Duration:</strong> ${duration}</p>
                        <p><strong>Project Type:</strong> ${deployment.projectType}</p>
                        <p><strong>Deployer:</strong> ${deployment.deployer}</p>
                    </div>
                </div>
                <hr>
                <h4>Deployment Log</h4>
                <div id="modal-log-${deployment.id}" style="max-height: 300px; overflow-y: auto; background-color: #f5f5f5; padding: 10px; font-family: Consolas, Monaco, monospace; font-size: 12px; border: 1px solid #ddd;">
                    <div style="color: #999;">Loading log...</div>
                </div>
            </div>
        `;

        const closeBtn = dialog.querySelector('.close');
        closeBtn.onclick = () => modal.remove();

        fetchDeploymentManifest(deployment.id).then(files => {
            const filesCountEl = document.getElementById(`modal-files-count-${deployment.id}`);
            if (filesCountEl && files) {
                filesCountEl.innerHTML = `<strong>Files:</strong> <a href="#" style="color: #337ab7;">${files.length} files</a>`;
                filesCountEl.querySelector('a').onclick = (e) => {
                    e.preventDefault();
                    modal.remove();
                    showManifestModal(files, deployment.id);
                };
            } else if (filesCountEl) {
                filesCountEl.innerHTML = '<strong>Files:</strong> <span style="color: #999;">N/A</span>';
            }
        });

        fetchDeploymentLog(deployment.id).then(logText => {
            const logContainer = document.getElementById(`modal-log-${deployment.id}`);
            if (logContainer && logText) {
                logContainer.innerHTML = '';
                const lines = logText.split('\n').filter(line => line.trim());
                
                lines.forEach(line => {
                    const parsed = parseLogLine(line);
                    if (!parsed) return;

                    const logLine = document.createElement('div');
                    logLine.style.padding = '2px 0';
                    logLine.style.borderBottom = '1px solid #e0e0e0';
                    
                    if (parsed.isIndented) {
                        logLine.style.paddingLeft = '40px';
                        logLine.style.backgroundColor = '#fafafa';
                    }

                    const time = new Date(parsed.timestamp);
                    const timeStr = time.toLocaleTimeString();

                    const timeSpan = document.createElement('span');
                    timeSpan.style.color = '#999';
                    timeSpan.style.marginRight = '10px';
                    timeSpan.textContent = timeStr;

                    const messageSpan = document.createElement('span');
                    messageSpan.textContent = parsed.message;
                    
                    if (parsed.message.toLowerCase().includes('error')) {
                        messageSpan.style.color = '#d9534f';
                        messageSpan.style.fontWeight = 'bold';
                    } else if (parsed.message.toLowerCase().includes('warning')) {
                        messageSpan.style.color = '#f0ad4e';
                    } else if (parsed.message.toLowerCase().includes('success')) {
                        messageSpan.style.color = '#5cb85c';
                        messageSpan.style.fontWeight = 'bold';
                    }

                    logLine.appendChild(timeSpan);
                    logLine.appendChild(messageSpan);
                    logContainer.appendChild(logLine);
                });

                logContainer.scrollTop = logContainer.scrollHeight;
            } else if (logContainer) {
                logContainer.innerHTML = '<div style="color: #999;">No log available</div>';
            }
        });

        modal.appendChild(dialog);
        document.body.appendChild(modal);
    }

    function formatDuration(start, end) {
        const startTime = new Date(start);
        const endTime = new Date(end);
        const duration = (endTime - startTime) / 1000;
        
        if (duration < 60) return `${Math.round(duration)}s`;
        if (duration < 3600) return `${Math.floor(duration / 60)}m ${Math.round(duration % 60)}s`;
        return `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
    }

    function renderLatestDeployment(statusData, activeId) {
        const isActive = statusData.id === activeId;

        const panel = document.createElement('div');
        const panelClass = statusData.status === 'Success' ? 'success' : statusData.status === 'Failed' ? 'danger' : 'info';
        panel.className = `panel panel-${panelClass}`;
        panel.style.marginBottom = '20px';

        const isValidTimeRange = statusData.startTime && statusData.endTime && 
                                 new Date(statusData.endTime) >= new Date(statusData.startTime);
        
        const duration = isValidTimeRange 
            ? formatDuration(statusData.startTime, statusData.endTime)
            : 'N/A';
        
        const completedTime = isValidTimeRange 
            ? new Date(statusData.endTime).toLocaleString()
            : 'N/A';

        panel.innerHTML = `
            <div class="panel-heading">
                <h3 class="panel-title">
                    Latest Deployment
                    ${isActive ? '<span class="label label-warning" style="margin-left: 10px;">ACTIVE</span>' : ''}
                </h3>
            </div>
            <div class="panel-body">
                <div class="row">
                    <div class="col-md-6">
                        <p><strong>Status:</strong> ${getStatusBadge(statusData.status)}</p>
                        <p><strong>ID:</strong> <code>${statusData.id}</code></p>
                        <p><strong>Author:</strong> ${statusData.author} (${statusData.authorEmail})</p>
                        <p><strong>Message:</strong> ${statusData.message}</p>
                        <p><strong>Complete:</strong> ${statusData.complete}</p>
                        <p id="files-count-${statusData.id}"><strong>Files:</strong> <span style="color: #999;">Loading...</span></p>
                    </div>
                    <div class="col-md-6">
                        <p><strong>Received:</strong> ${statusData.receivedTime ? new Date(statusData.receivedTime).toLocaleString() : 'N/A'}</p>
                        <p><strong>Started:</strong> ${statusData.startTime ? new Date(statusData.startTime).toLocaleString() : 'N/A'}</p>
                        <p><strong>Completed:</strong> ${completedTime}</p>
                        <p><strong>Duration:</strong> ${duration}</p>
                        <p><strong>Project Type:</strong> ${statusData.projectType}</p>
                        <p><strong>Deployer:</strong> ${statusData.deployer}</p>
                    </div>
                </div>
            </div>
        `;

        fetchDeploymentManifest(statusData.id).then(files => {
            const filesCountEl = document.getElementById(`files-count-${statusData.id}`);
            if (filesCountEl && files) {
                filesCountEl.innerHTML = `<strong>Files:</strong> <a href="#" style="color: #337ab7;">${files.length} files</a>`;
                filesCountEl.querySelector('a').onclick = (e) => {
                    e.preventDefault();
                    showManifestModal(files, statusData.id);
                };
            } else if (filesCountEl) {
                filesCountEl.innerHTML = '<strong>Files:</strong> <span style="color: #999;">N/A</span>';
            }
        });

        return panel;
    }

    async function fetchDeploymentLog(id) {
        try {
            const res = await fetch(`/api/vfs/site/deployments/${id}/log.log`);
            if (!res.ok) return null;
            const text = await res.text();
            return text;
        } catch (err) {
            return null;
        }
    }

    async function fetchDeploymentManifest(id) {
        try {
            const res = await fetch(`/api/vfs/site/deployments/${id}/manifest`);
            if (!res.ok) return null;
            const text = await res.text();
            return text.split('\n').filter(line => line.trim());
        } catch (err) {
            return null;
        }
    }

    function showManifestModal(files, deploymentId) {
        const modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '0';
        modal.style.left = '0';
        modal.style.width = '100%';
        modal.style.height = '100%';
        modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
        modal.style.zIndex = '10000';
        modal.style.display = 'flex';
        modal.style.alignItems = 'center';
        modal.style.justifyContent = 'center';
        modal.onclick = (e) => {
            if (e.target === modal) modal.remove();
        };

        const dialog = document.createElement('div');
        dialog.className = 'panel panel-default';
        dialog.style.width = '80%';
        dialog.style.maxWidth = '900px';
        dialog.style.maxHeight = '90%';
        dialog.style.overflow = 'hidden';
        dialog.style.margin = '0';
        dialog.style.display = 'flex';
        dialog.style.flexDirection = 'column';

        const header = document.createElement('div');
        header.className = 'panel-heading';
        header.innerHTML = `
            <h3 class="panel-title">
                Deployed Files (${files.length})
                <button type="button" class="close" style="margin-top: -2px;">&times;</button>
            </h3>
        `;
        dialog.appendChild(header);

        const body = document.createElement('div');
        body.className = 'panel-body';
        body.style.overflowY = 'auto';
        body.style.flex = '1';
        body.style.fontFamily = 'Consolas, Monaco, monospace';
        body.style.fontSize = '12px';

        const searchBox = document.createElement('input');
        searchBox.type = 'text';
        searchBox.className = 'form-control';
        searchBox.placeholder = 'Filter files...';
        searchBox.style.marginBottom = '10px';
        body.appendChild(searchBox);

        const fileList = document.createElement('div');
        fileList.id = 'manifest-file-list';
        body.appendChild(fileList);

        const renderFiles = (filter = '') => {
            const filteredFiles = filter 
                ? files.filter(f => f.toLowerCase().includes(filter.toLowerCase()))
                : files;

            fileList.innerHTML = '';

            const directories = {};
            const rootFiles = [];

            filteredFiles.forEach(file => {
                if (file.includes('\\') || file.includes('/')) {
                    const separator = file.includes('\\') ? '\\' : '/';
                    const parts = file.split(separator);
                    const dir = parts[0];
                    if (!directories[dir]) directories[dir] = [];
                    directories[dir].push(file);
                } else {
                    rootFiles.push(file);
                }
            });

            if (rootFiles.length > 0) {
                const rootHeader = document.createElement('div');
                rootHeader.style.fontWeight = 'bold';
                rootHeader.style.marginTop = '10px';
                rootHeader.style.marginBottom = '5px';
                rootHeader.style.color = '#333';
                rootHeader.textContent = `Root (${rootFiles.length} files)`;
                fileList.appendChild(rootHeader);

                rootFiles.forEach(file => {
                    const fileDiv = document.createElement('div');
                    fileDiv.style.padding = '2px 0 2px 20px';
                    fileDiv.style.color = '#666';
                    fileDiv.textContent = file;
                    fileList.appendChild(fileDiv);
                });
            }

            Object.keys(directories).sort().forEach(dir => {
                const dirHeader = document.createElement('div');
                dirHeader.style.fontWeight = 'bold';
                dirHeader.style.marginTop = '10px';
                dirHeader.style.marginBottom = '5px';
                dirHeader.style.color = '#333';
                dirHeader.style.cursor = 'pointer';
                dirHeader.innerHTML = `<span style="margin-right: 5px;">▼</span>${dir} (${directories[dir].length} files)`;
                
                const filesContainer = document.createElement('div');
                filesContainer.style.display = 'block';
                
                directories[dir].forEach(file => {
                    const fileDiv = document.createElement('div');
                    fileDiv.style.padding = '2px 0 2px 20px';
                    fileDiv.style.color = '#666';
                    fileDiv.textContent = file;
                    filesContainer.appendChild(fileDiv);
                });

                dirHeader.onclick = () => {
                    if (filesContainer.style.display === 'none') {
                        filesContainer.style.display = 'block';
                        dirHeader.innerHTML = `<span style="margin-right: 5px;">▼</span>${dir} (${directories[dir].length} files)`;
                    } else {
                        filesContainer.style.display = 'none';
                        dirHeader.innerHTML = `<span style="margin-right: 5px;">▶</span>${dir} (${directories[dir].length} files)`;
                    }
                };

                fileList.appendChild(dirHeader);
                fileList.appendChild(filesContainer);
            });

            if (filteredFiles.length === 0) {
                fileList.innerHTML = '<div style="color: #999; padding: 20px; text-align: center;">No files match the filter</div>';
            }
        };

        searchBox.oninput = (e) => renderFiles(e.target.value);
        renderFiles();

        dialog.appendChild(body);

        const closeBtn = dialog.querySelector('.close');
        closeBtn.onclick = () => modal.remove();

        modal.appendChild(dialog);
        document.body.appendChild(modal);
    }

    function parseLogLine(line) {
        const isIndented = line.startsWith('\t');
        const cleanLine = isIndented ? line.substring(1) : line;
        
        const parts = cleanLine.split(',');
        if (parts.length < 4) return null;
        
        const timestamp = parts[0];
        const message = parts.slice(1, -2).join(',');
        const level = parts[parts.length - 1];
        
        return {
            timestamp,
            message,
            level,
            isIndented
        };
    }

    function renderDeploymentLog(logText, deploymentId) {
        const panel = document.createElement('div');
        panel.className = 'panel panel-default';
        panel.style.marginBottom = '20px';
        panel.id = 'deployment-log-panel';

        const header = document.createElement('div');
        header.className = 'panel-heading';
        header.innerHTML = `
            <h3 class="panel-title">
                Deployment Log
                <span id="log-loading-indicator" style="margin-left: 10px; display: none;">
                    <span class="label label-warning">Loading...</span>
                </span>
                <div class="pull-right">
                    <button class="btn btn-xs btn-primary" id="auto-refresh-log-btn" style="margin-right: 5px;">Auto-refresh</button>
                    <button class="btn btn-xs btn-default" id="toggle-log-btn">Collapse</button>
                </div>
            </h3>
        `;
        panel.appendChild(header);

        const body = document.createElement('div');
        body.className = 'panel-body';
        body.id = 'log-panel-body';
        body.style.maxHeight = '500px';
        body.style.overflowY = 'auto';
        body.style.backgroundColor = '#f5f5f5';
        body.style.fontFamily = 'Consolas, Monaco, monospace';
        body.style.fontSize = '12px';

        const lines = logText.split('\n').filter(line => line.trim());
        
        lines.forEach(line => {
            const parsed = parseLogLine(line);
            if (!parsed) return;

            const logLine = document.createElement('div');
            logLine.style.padding = '2px 0';
            logLine.style.borderBottom = '1px solid #e0e0e0';
            
            if (parsed.isIndented) {
                logLine.style.paddingLeft = '40px';
                logLine.style.backgroundColor = '#fafafa';
            }

            const time = new Date(parsed.timestamp);
            const timeStr = time.toLocaleTimeString();

            const timeSpan = document.createElement('span');
            timeSpan.style.color = '#999';
            timeSpan.style.marginRight = '10px';
            timeSpan.textContent = timeStr;

            const messageSpan = document.createElement('span');
            messageSpan.textContent = parsed.message;
            
            if (parsed.message.toLowerCase().includes('error')) {
                messageSpan.style.color = '#d9534f';
                messageSpan.style.fontWeight = 'bold';
            } else if (parsed.message.toLowerCase().includes('warning')) {
                messageSpan.style.color = '#f0ad4e';
            } else if (parsed.message.toLowerCase().includes('success')) {
                messageSpan.style.color = '#5cb85c';
                messageSpan.style.fontWeight = 'bold';
            }

            logLine.appendChild(timeSpan);
            logLine.appendChild(messageSpan);
            body.appendChild(logLine);
        });

        panel.appendChild(body);

        currentDeploymentId = deploymentId;

        setTimeout(() => {
            const toggleBtn = document.getElementById('toggle-log-btn');
            const autoRefreshBtn = document.getElementById('auto-refresh-log-btn');
            const logBody = document.getElementById('log-panel-body');
            
            if (toggleBtn && logBody) {
                toggleBtn.onclick = () => {
                    if (logBody.style.display === 'none') {
                        logBody.style.display = 'block';
                        toggleBtn.textContent = 'Collapse';
                    } else {
                        logBody.style.display = 'none';
                        toggleBtn.textContent = 'Expand';
                    }
                };
                logBody.scrollTop = logBody.scrollHeight;
            }

            if (autoRefreshBtn) {
                autoRefreshBtn.onclick = () => {
                    if (logRefreshInterval) {
                        stopLogRefresh();
                    } else {
                        startLogRefresh();
                    }
                };
            }
        }, 0);

        return panel;
    }

    async function startLogRefresh() {
        const autoRefreshBtn = document.getElementById('auto-refresh-log-btn');
        
        if (autoRefreshBtn) {
            autoRefreshBtn.textContent = 'Stop Auto-refresh';
            autoRefreshBtn.className = 'btn btn-xs btn-danger';
        }

        if (currentDeploymentId) {
            await refreshLogAndStatus(currentDeploymentId);
        }

        logRefreshInterval = setInterval(async () => {
            if (currentDeploymentId) {
                await refreshLogAndStatus(currentDeploymentId);
            }
        }, 5000);
    }

    function stopLogRefresh() {
        const autoRefreshBtn = document.getElementById('auto-refresh-log-btn');
        
        if (logRefreshInterval) {
            clearInterval(logRefreshInterval);
            logRefreshInterval = null;
        }

        if (autoRefreshBtn) {
            autoRefreshBtn.textContent = 'Auto-refresh';
            autoRefreshBtn.className = 'btn btn-xs btn-primary';
        }
    }

    async function refreshLogContent(deploymentId) {
        const logBody = document.getElementById('log-panel-body');
        const loadingIndicator = document.getElementById('log-loading-indicator');
        if (!logBody) return;

        if (loadingIndicator) {
            loadingIndicator.style.display = 'inline';
        }

        const wasAtBottom = logBody.scrollHeight - logBody.scrollTop <= logBody.clientHeight + 50;

        const logText = await fetchDeploymentLog(deploymentId);
        if (!logText) return;

        logBody.innerHTML = '';

        const lines = logText.split('\n').filter(line => line.trim());
        
        lines.forEach(line => {
            const parsed = parseLogLine(line);
            if (!parsed) return;

            const logLine = document.createElement('div');
            logLine.style.padding = '2px 0';
            logLine.style.borderBottom = '1px solid #e0e0e0';
            
            if (parsed.isIndented) {
                logLine.style.paddingLeft = '40px';
                logLine.style.backgroundColor = '#fafafa';
            }

            const time = new Date(parsed.timestamp);
            const timeStr = time.toLocaleTimeString();

            const timeSpan = document.createElement('span');
            timeSpan.style.color = '#999';
            timeSpan.style.marginRight = '10px';
            timeSpan.textContent = timeStr;

            const messageSpan = document.createElement('span');
            messageSpan.textContent = parsed.message;
            
            if (parsed.message.toLowerCase().includes('error')) {
                messageSpan.style.color = '#d9534f';
                messageSpan.style.fontWeight = 'bold';
            } else if (parsed.message.toLowerCase().includes('warning')) {
                messageSpan.style.color = '#f0ad4e';
            } else if (parsed.message.toLowerCase().includes('success')) {
                messageSpan.style.color = '#5cb85c';
                messageSpan.style.fontWeight = 'bold';
            }

            logLine.appendChild(timeSpan);
            logLine.appendChild(messageSpan);
            logBody.appendChild(logLine);
        });

        if (wasAtBottom) {
            logBody.scrollTop = logBody.scrollHeight;
        }

        if (loadingIndicator) {
            loadingIndicator.style.display = 'none';
        }
    }

    async function refreshLogAndStatus(deploymentId) {
        await refreshLogContent(deploymentId);
        
        const statusData = await fetchDeploymentStatus(deploymentId);
        
        if (statusData) {
            updateLatestDeploymentStatus(statusData);
            
            const allowedStatuses = ['Pending', 'Building', 'Deploying'];
            if (!allowedStatuses.includes(statusData.status)) {
                stopLogRefresh();
            }
        }
    }

    function updateLatestDeploymentStatus(statusData) {
        const latestPanel = document.querySelector('.panel.panel-success, .panel.panel-danger, .panel.panel-info');
        if (!latestPanel) return;
        
        const panelClass = statusData.status === 'Success' ? 'success' : statusData.status === 'Failed' ? 'danger' : 'info';
        latestPanel.className = `panel panel-${panelClass}`;
        
        const statusElements = latestPanel.querySelectorAll('p');
        statusElements.forEach(p => {
            const strong = p.querySelector('strong');
            if (!strong) return;
            
            const label = strong.textContent;
            if (label === 'Status:') {
                const statusBadge = p.querySelector('.label');
                if (statusBadge) {
                    p.innerHTML = `<strong>Status:</strong> ${getStatusBadge(statusData.status)}`;
                }
            } else if (label === 'Completed:') {
                const isValidTimeRange = statusData.startTime && statusData.endTime && 
                                         new Date(statusData.endTime) >= new Date(statusData.startTime);
                const completedTime = isValidTimeRange ? new Date(statusData.endTime).toLocaleString() : 'N/A';
                p.innerHTML = `<strong>Completed:</strong> ${completedTime}`;
            } else if (label === 'Duration:') {
                const isValidTimeRange = statusData.startTime && statusData.endTime && 
                                         new Date(statusData.endTime) >= new Date(statusData.startTime);
                const duration = isValidTimeRange ? formatDuration(statusData.startTime, statusData.endTime) : 'N/A';
                p.innerHTML = `<strong>Duration:</strong> ${duration}`;
            } else if (label === 'Complete:') {
                p.innerHTML = `<strong>Complete:</strong> ${statusData.complete}`;
            }
        });
    }

    function renderDeploymentList(deployments, activeId) {
        const container = document.createElement('div');
        
        const header = document.createElement('h2');
        header.textContent = 'Deployment History';
        header.style.marginTop = '30px';
        header.style.marginBottom = '15px';
        container.appendChild(header);

        if (deployments.length === 0) {
            const alert = document.createElement('div');
            alert.className = 'alert alert-info';
            alert.textContent = 'No deployment history found.';
            container.appendChild(alert);
            return container;
        }

        const table = document.createElement('table');
        table.className = 'table table-striped table-hover';
        table.innerHTML = `
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Status</th>
                    <th>Message</th>
                    <th>Date</th>
                    <th>Active</th>
                </tr>
            </thead>
            <tbody id="deployment-list-body"></tbody>
        `;
        container.appendChild(table);

        const tbody = table.querySelector('#deployment-list-body');
        
        deployments.forEach(dep => {
            const isActive = dep.id === activeId;
            const row = document.createElement('tr');
            row.style.cursor = 'pointer';
            if (isActive) {
                row.style.backgroundColor = '#fcf8e3';
                row.style.fontWeight = 'bold';
            }
            
            row.innerHTML = `
                <td><code>${dep.id.substring(0, 8)}</code></td>
                <td id="status-${dep.id}">${getStatusBadge('Loading')}</td>
                <td id="message-${dep.id}">Loading...</td>
                <td>${dep.date ? new Date(dep.date).toLocaleString() : 'Unknown'}</td>
                <td>${isActive ? '<span class="label label-warning">ACTIVE</span>' : ''}</td>
            `;
            
            row.onclick = async () => {
                const statusData = await fetchDeploymentStatus(dep.id);
                if (statusData) {
                    showDeploymentDetails(statusData, activeId);
                }
            };
            
            tbody.appendChild(row);
            
            fetchDeploymentStatus(dep.id).then(statusData => {
                if (statusData) {
                    const statusCell = document.getElementById(`status-${dep.id}`);
                    const messageCell = document.getElementById(`message-${dep.id}`);
                    if (statusCell) statusCell.innerHTML = getStatusBadge(statusData.status);
                    if (messageCell) messageCell.textContent = statusData.message || 'N/A';
                }
            });
        });

        return container;
    }

    async function triggerNewDeployment() {
        const triggerBtn = event.target;
        const originalText = triggerBtn.textContent;
        triggerBtn.disabled = true;
        triggerBtn.textContent = 'Triggering...';

        try {
            const responseFetch = fetch('/api/deployments', {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                }
            });

            setTimeout(() => {
                const alert = document.createElement('div');
                alert.className = 'alert alert-success';
                alert.textContent = 'Deployment triggered successfully!';
                alert.style.position = 'fixed';
                alert.style.top = '70px';
                alert.style.right = '20px';
                alert.style.zIndex = '9999';
                alert.style.minWidth = '300px';
                document.body.appendChild(alert);
                
                setTimeout(() => alert.remove(), 3000);
                
                fetchDeploymentData();
            }, 500);

            const reponse = await responseFetch;
            if (response.ok) {
            } else {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
        } catch (err) {
            const alert = document.createElement('div');
            alert.className = 'alert alert-danger';
            alert.textContent = `Failed to trigger deployment: ${err.message}`;
            alert.style.position = 'fixed';
            alert.style.top = '70px';
            alert.style.right = '20px';
            alert.style.zIndex = '9999';
            alert.style.minWidth = '300px';
            document.body.appendChild(alert);
            
            setTimeout(() => alert.remove(), 5000);
        } finally {
            triggerBtn.disabled = false;
            triggerBtn.textContent = originalText;
        }
    }

    async function fetchDeploymentData() {
        const content = document.getElementById('deployment-content');
        content.innerHTML = '<div class="alert alert-info">Loading deployment data...</div>';

        try {
            const [activeRes, deploymentsRes] = await Promise.all([
                fetch('/api/vfs/site/deployments/active'),
                fetch('/api/vfs/site/deployments/')
            ]);

            let activeId = null;
            if (activeRes.ok) {
                activeId = (await activeRes.text()).trim();
            }

            let deployments = [];
            let latestDeploymentId = null;
            if (deploymentsRes.ok) {
                const items = await deploymentsRes.json();
                
                deployments = items
                    .filter(item => {
                        return item.mime === 'inode/directory' && 
                               item.name !== 'tools';
                    })
                    .map(item => {
                        return {
                            id: item.name,
                            date: item.mtime
                        };
                    })
                    .sort((a, b) => {
                        const dateA = new Date(a.date);
                        const dateB = new Date(b.date);
                        return dateB - dateA;
                    });
                
                if (deployments.length > 0) {
                    latestDeploymentId = deployments[0].id;
                }
            }

            content.innerHTML = '';

            if (latestDeploymentId) {
                const latestStatusData = await fetchDeploymentStatus(latestDeploymentId);
                
                if (latestStatusData) {
                    content.appendChild(renderLatestDeployment(latestStatusData, activeId));
                    
                    const logText = await fetchDeploymentLog(latestDeploymentId);
                    if (logText) {
                        content.appendChild(renderDeploymentLog(logText, latestDeploymentId));
                    } else {
                        const alert = document.createElement('div');
                        alert.className = 'alert alert-info';
                        alert.textContent = 'No deployment log available.';
                        alert.style.marginBottom = '20px';
                        content.appendChild(alert);
                    }
                } else {
                    const alert = document.createElement('div');
                    alert.className = 'alert alert-warning';
                    alert.textContent = 'Could not load latest deployment status.';
                    content.appendChild(alert);
                }
            } else {
                const alert = document.createElement('div');
                alert.className = 'alert alert-warning';
                alert.textContent = 'No deployments found.';
                content.appendChild(alert);
            }

            content.appendChild(renderDeploymentList(deployments, activeId));

        } catch (err) {
            content.innerHTML = `<div class="alert alert-danger">Failed to load deployment data: ${err.message}</div>`;
        }
    }

    function updateNavbarState() {
        const currentState = history.state;
        const deploymentLink = document.getElementById('deployment-nav-link');
        
        if (deploymentLink) {
            if (currentState && currentState.view === 'deployments') {
                deploymentLink.classList.add('active');
            } else {
                deploymentLink.classList.remove('active');
            }
        }
    }

    window.addEventListener('viewer-change', (event) => {
        if (event.detail.viewer !== 'deployments' && isViewerActive) {
            isViewerActive = false;
        }
    });

    window.addEventListener('load', () => {
        addNavbarLink();

        if (window.location.search.includes('deployments')) {
            showDeploymentViewer(true);
        }
        
        updateNavbarState();
    });

    window.addEventListener('popstate', (event) => {
        if (event.state && event.state.view === 'deployments') {
            if (!isViewerActive) {
                showDeploymentViewer(true);
            }
        } else if (event.state && event.state.view !== 'deployments') {
            if (isViewerActive) {
                hideDeploymentViewer(true);
            }
        } else if (!event.state || !event.state.view) {
            if (isViewerActive) {
                hideDeploymentViewer(true);
            }
        }
        
        updateNavbarState();
    });

})();