Umbraco Cloud Deployment Viewer

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
    });

})();