Your IP : 216.73.216.224


Current Path : /var/www/html/administrator/components/com_jdownloads/helpers/
Upload File :
Current File : /var/www/html/administrator/components/com_jdownloads/helpers/scan-interface.php

<?php
/**
 * @package jDownloads
 * @version 4.1
 * Modern Monitoring Interface with AJAX Progress
 */

define('_JEXEC', 1);

if (!defined('DS')){
    define( 'DS', DIRECTORY_SEPARATOR );
}

define('JPATH', dirname(__FILE__) );

$parts = explode( DS, JPATH );
$script_root =  implode( DS, $parts ) ;

// check path
$x = array_search ( 'administrator', $parts  );
if (!$x) exit;

$path = '';
for ($i=0; $i < $x; $i++){
    $path = $path.$parts[$i].'/';
}
// remove last DS
$path = substr($path, 0, -1);

if (!defined('JPATH_BASE')){
    define('JPATH_BASE', $path );
}

setlocale(LC_ALL, 'C.UTF-8', 'C');

// Run the application
require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

// Boot the DI container
$container = \Joomla\CMS\Factory::getContainer();

// Alias session services
$container->alias('session.web', 'session.web.site')
    ->alias('session', 'session.web.site')
    ->alias('JSession', 'session.web.site')
    ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site')
    ->alias(\Joomla\Session\Session::class, 'session.web.site')
    ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site');

// Instantiate the application.
$app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class);
$app->createExtensionNamespaceMap();
\Joomla\CMS\Factory::$application = $app;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Component\ComponentHelper;

// Backend language to prefer for admin scripts
$backend_lang = ComponentHelper::getParams('com_languages')->get('administrator', 'en-GB');

// Language loading
$lang = Factory::getLanguage();
$lang->load('com_jdownloads', JPATH_ADMINISTRATOR, $backend_lang, true);

// Get parameters
$params = ComponentHelper::getParams('com_jdownloads');
$secret = $params->get('scan_secret_key');

$jinput = Factory::getApplication()->getInput();
$mode = 0;  // Always Mode 0 - scan everything
$testrun = $jinput->get('test', 0, 'int');

// $modeLabels = [    0 => Text::_('COM_JDOWNLOADS_MONITORING_MODE_0'),];

// Create date string   
$date = Factory::getDate();
$tz = Factory::getConfig()->get( 'offset' );
$date->setTimezone(new DateTimeZone($tz));
$date = date(Text::_('DATE_FORMAT_LC2'));
$date_string = Text::_('COM_JDOWNLOADS_LOGS_COL_DATE_LABEL') . ': ' . $date;

// Create testrun string
$testrunString = Text::_('COM_JDOWNLOADS_AUTO_CHECK_TEST_RUN_HINT');

// Log title string
$logTitleString = Text::_('COM_JDOWNLOADS_MONITORING_LOG');

$runChangeStartString = Text::_('COM_JDOWNLOADS_AUTOCHECK_MAKE_CHANGES_PERMANENTLY');

// Create finished info string
$finishedInfo = addslashes(Text::_('COM_JDOWNLOADS_AUTO_CHECK_FINISH_INFO_TEST'));
$finishedInfo = strip_tags($finishedInfo);

$cronInfoHtml = Text::_('COM_JDOWNLOADS_RUN_MONITORING_INFO8');

?>
<!DOCTYPE html>
<html lang="<?php echo Factory::getLanguage()->getTag(); ?>">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_BUTTON_TEXT'); ?></title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            padding: 20px;
            background: #f8f9fa;
        }
        .monitor-container {
            max-width: 900px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            padding: 30px;
        }
        .progress {
            height: 30px;
            font-size: 14px;
            margin-bottom: 20px;
        }
        .progress-bar {
            transition: width 0.3s ease;
            font-weight: 600;
        }
        .log-container {
            max-height: 400px;
            overflow-y: auto;
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 4px;
            padding: 15px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }
        .log-entry {
            padding: 4px 0;
            border-bottom: 1px solid #e9ecef;
        }
        .log-entry:last-child {
            border-bottom: none;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: 1fr; /* Mobile: 1 Spalte */
            gap: 15px;
            margin: 10px 0;
        }
        /* Tablet und größer: 2 Spalten */
        @media (min-width: 576px) {
            .stats-grid {
                grid-template-columns: repeat(2, 1fr);
            }
            /* Wenn nur 1 Card: volle Breite über beide Spalten */
            .stats-grid .stat-card:only-child {
                grid-column: 1 / -1;
            }
        }
        .stat-card {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 12px 15px;
            border-radius: 8px;
            text-align: center;
        }
        .stat-card.success {
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
        }
        .stat-card.warning {
            background: linear-gradient(135deg, #ff7664ff 0%, #f5576c 100%);
        }
        .stat-value {
            font-size: 28px;
            font-weight: bold;
            margin: 3px 0;
        }
        .stat-label {
            font-size: 16px;
            opacity: 0.9;
        }
        .btn:disabled {
            background-color: #6c757d !important;
            border-color: #6c757d !important;
            color: #f8f9fa !important;
            opacity: 0.65;
            cursor: not-allowed;
        }
        .btn:disabled:hover {
            background-color: #6c757d !important;
            border-color: #6c757d !important;
        }
        .phase-badge {
            display: inline-block;
            padding: 6px 12px;
            border-radius: 20px;
            font-size: 13px;
            font-weight: 600;
            margin-bottom: 15px;
        }
        .phase-initializing { background: #ffc107; color: #000; }
        .phase-scanning { background: #17a2b8; color: #fff; }
        .phase-completed { background: #28a745; color: #fff; }
        .spinner {
            display: inline-block;
            width: 16px;
            height: 16px;
            border: 2px solid #f3f3f3;
            border-top: 2px solid #3498db;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>

<div class="monitor-container">
    <h4 class="mb-2">
        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-search me-2" viewBox="0 0 16 16">
            <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
        </svg>
        <?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_TITLE'); ?>
        <?php if ($testrun): ?>
        <span class="badge bg-warning text-dark ms-2 small"><?php echo Text::_('COM_JDOWNLOADS_AUTO_CHECK_TEST_RUN_HINT'); ?></span>
        <?php endif; ?>
    </h4>
    
    <!-- <div class="alert alert-light">
        <strong><?php echo Text::_('COM_JDOWNLOADS_MONITORING_MODE'); ?>:</strong> 
        <?php echo $modeLabels[$mode] ?? 'Unknown'; ?>
        <?php if ($testrun): ?>
        <span class="badge bg-warning text-dark ms-2"><?php echo Text::_('COM_JDOWNLOADS_AUTO_CHECK_TEST_RUN_HINT'); ?></span>
        <?php endif; ?>
    </div> -->

    <div id="phaseIndicator"></div>

    <!-- Progress bar with two-line status -->
    <div class="progress mb-3" style="height: 50px; position: relative;">
        <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-info" 
             role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
        </div>
        <div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2; pointer-events: none;">
            <div id="currentStatus" style="font-size: 13px; color: #495057; line-height: 1.2;">
                <span class="spinner me-2"></span><?php echo Text::_('COM_JDOWNLOADS_MONITORING_INITIALIZING'); ?>
            </div>
            <div id="progressPercent" style="font-weight: 600; font-size: 14px; color: #212529; margin-top: 2px;">
                0%
            </div>
        </div>
    </div>

    <!-- Statistics -->
    <div class="stats-grid" id="statsGrid">
        <div class="stat-card success">
            <div class="stat-label"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_NEW_CATS'); ?></div>
            <div class="stat-value" id="stat-new-cats">0</div>
        </div>
        <div class="stat-card success">
            <div class="stat-label"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_NEW_DOWNLOADS'); ?></div>
            <div class="stat-value" id="stat-new-downloads">0</div>
        </div>
        <div class="stat-card warning">
            <div class="stat-label"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_MISSING_CATS'); ?></div>
            <div class="stat-value" id="stat-missing-cats">0</div>
        </div>
        <div class="stat-card warning">
            <div class="stat-label"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_MISSING_FILES'); ?></div>
            <div class="stat-value" id="stat-missing-files">0</div>
        </div>
    </div>

    <!-- Log -->
    <div class="stats-grid" id="statsGrid">
        <div class="">
            <h5 class="mt-2">
            <?php echo Text::_('COM_JDOWNLOADS_MONITORING_LOG'); ?>
            </h5>
        </div>
        <div class="">        
            <h5 class="mt-2">    
                <button id="btnDownloadLog" class="btn btn-outline-secondary btn-sm float-end" style="display:none;">
                    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
                        <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
                        <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
                    </svg>
                    <?php echo Text::_('COM_JDOWNLOADS_MONITORING_LOG_DOWNLOAD'); ?>
                </button>
            </h5>   
        </div>
    </div>
    <div id="btnDownloadLogHint" class="mb-2 small" style="display:none;"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_LOG_DOWNLOAD_HINT'); ?></div>
    <div class="log-container" id="logContainer">
        <div class="log-entry text-muted"><?php echo Text::_('COM_JDOWNLOADS_MONITORING_LOG_WAITING'); ?></div>
    </div>

    <div id="scanSummaryInfo" class="alert alert-success small mt-3" style="display:none;"></div>

    <!-- Controls -->
    <div class="text-center mt-4">
        <button id="btnStart" class="btn btn-primary btn-sm">
            <!--<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill me-2" viewBox="0 0 16 16">
                <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/>
            </svg>-->
            <?php echo Text::_('COM_JDOWNLOADS_MONITORING_START_SCAN'); ?>
        </button>
        <?php if ($testrun): ?>
        <button id="btnRunReal" class="btn btn-warning btn-sm" style="display:none;">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill me-2" viewBox="0 0 16 16">
                <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
            </svg>
            <?php echo $runChangeStartString; ?>
        </button>
        <?php endif; ?>
        <button id="btnReset" class="btn btn-secondary btn-sm" style="display:none;">
            <?php echo Text::_('COM_JDOWNLOADS_MONITORING_RESET'); ?>
        </button>
    </div>
</div>

<script>
const scanMonitor = {
    workerUrl: 'scan-worker.php',
    secret: '<?php echo $secret; ?>',
    mode: <?php echo $mode; ?>,
    testrun: <?php echo $testrun; ?>,
    dateStr: '<?php echo $date_string; ?>',
    testrunStr: '<?php echo $testrunString; ?>',
    logTitleStr: '<?php echo $logTitleString; ?>',
    runScanStr: '<?php echo Text::_('COM_JDOWNLOADS_MONITORING_START_SCAN') . '...'; ?>',
    scanCompleteStr: '✓ ' + '<?php echo Text::_('COM_JDOWNLOADS_MONITORING_LOG_FINISHED'); ?>',
    resultsHintStr: '<?php echo $finishedInfo; ?>',
    cronInfoHtml: <?php echo json_encode($cronInfoHtml); ?>,
    initStr: '<?php echo Text::_('COM_JDOWNLOADS_MONITORING_INITIALIZING'); ?>',
    processedSuffixStr: '<?php echo Text::_('COM_JDOWNLOADS_UPLOAD_PROCESSED'); ?>',
    checkOnlyExistCatsStr: '<?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_INFO4'); ?>',
    checkOnlyExistFilesStr: '<?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_INFO6'); ?>',
    searchOnlyNewCatsStr: '<?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_INFO3'); ?>',
    searchOnlyNewFilesStr: '<?php echo Text::_('COM_JDOWNLOADS_RUN_MONITORING_INFO5'); ?>',
    pollInterval: null,
    scanFinished: false,
    lastAppliedStateVersion: -1,
    processRequestInFlight: false,
    
    init() {
        document.getElementById('btnStart').addEventListener('click', () => this.startScan());
        document.getElementById('btnReset').addEventListener('click', () => this.resetScan());
        document.getElementById('btnDownloadLog').addEventListener('click', () => this.downloadCompleteLog());
        
        const btnRunReal = document.getElementById('btnRunReal');
        if (btnRunReal) {
            btnRunReal.addEventListener('click', () => this.runRealScan());
        }
    },
    
    downloadCompleteLog() {
        // Fetch complete log from server (not just the 100 displayed entries)
        fetch(`${this.workerUrl}?action=export_log&key=${this.secret}`)
            .then(response => {
                if (!response.ok) {
                    throw new Error('Failed to fetch log');
                }
                return response.json();
            })
            .then(data => {
                if (!data.success || !data.log) {
                    alert('No log available for download.');
                    return;
                }
                
                // Build text content with ALL log entries from server
                let logText = this.logTitleStr + '\n';
                logText += '='.repeat(50) + '\n';
                logText += this.dateStr + '\n';
                //logText += this.modusString + '\n';
                logText += this.testrunStr + '\n';
                logText += '='.repeat(50) + '\n\n';
                
                // data.log contains ALL entries (not limited to 100)
                data.log.forEach(entry => {
                    logText += entry + '\n';
                });
                
                // Create download
                const blob = new Blob([logText], { type: 'text/plain;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'jdownloads-scan-' + new Date().toISOString().split('T')[0] + '.txt';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            })
            .catch(error => {
                console.error('Download failed:', error);
                alert('Failed to download log. Please try again.');
            });
    },
    
    runRealScan() {
        // Reload page without test parameter
        const url = new URL(window.location);
        url.searchParams.set('test', '0');
        window.location.href = url.toString();
    },
    
    async startScan() {
        document.getElementById('btnStart').disabled = true;
        this.scanFinished = false;
        this.lastAppliedStateVersion = -1;
        this.processRequestInFlight = false;
        
        try {
            // Initialize scan
            const response = await fetch(`${this.workerUrl}?action=start&key=${this.secret}&mode=${this.mode}&test=${this.testrun}`);
            const data = await response.json();
            
            if (data.success) {
                this.startPolling();
            } else {
                this.addLog('ERROR: ' + data.error, 'danger');
            }
        } catch (error) {
            this.addLog('ERROR: ' + error.message, 'danger');
        }
    },
    
    startPolling() {
        this.pollInterval = setInterval(() => this.pollStatus(), 1000);
        // Start first chunk immediately
        this.processChunk();
    },
    
    async processChunk() {
        if (this.scanFinished || this.processRequestInFlight) {
            return;
        }

        this.processRequestInFlight = true;

        try {
            const response = await fetch(`${this.workerUrl}?action=process&key=${this.secret}`);
            const data = await response.json();
            
            if (data.success) {
                // Update UI immediately
                this.updateUI(data.data);
                
                // Continue processing if not completed
                if (!data.data.completed) {
                    // Keep a short gap so the UI stays responsive without adding much idle time.
                    setTimeout(() => this.processChunk(), 100);
                } else {
                    // Scan completed
                    this.onScanComplete(data.data);
                }
            } else {
                console.error('Process error:', data.error);
                this.addLog('ERROR: ' + (data.error || 'Unknown Error'), 'danger');
            }
        } catch (error) {
            console.error('Process error:', error);
            this.addLog('ERROR: ' + error.message, 'danger');
        } finally {
            this.processRequestInFlight = false;
        }
    },
    
    async pollStatus() {
        try {
            const response = await fetch(`${this.workerUrl}?action=status&key=${this.secret}`);
            const data = await response.json();
            
            if (data.success) {
                if (this.scanFinished && !data.data.completed) {
                    return;
                }

                this.updateUI(data.data);
            }
        } catch (error) {
            console.error('Poll error:', error);
        }
    },
    
    updateUI(scanData) {
        if (this.scanFinished && !scanData.completed) {
            return;
        }

        const incomingStateVersion = Number(scanData.state_version ?? 0);

        if (incomingStateVersion < this.lastAppliedStateVersion) {
            return;
        }

        this.lastAppliedStateVersion = incomingStateVersion;

        const phaseIndicator = document.getElementById('phaseIndicator');

        // Update progress bar
        const progress = this.getOverallProgress(scanData);
        const progressBar = document.getElementById('progressBar');
        const progressPercent = document.getElementById('progressPercent');
        progressBar.style.width = progress + '%';
        progressPercent.textContent = progress + '%';
        progressBar.setAttribute('aria-valuenow', progress);
        
        // Update phase with labels
        const phaseLabels = {
            'initializing': this.initStr,
            'scanning_folders': this.searchOnlyNewCatsStr,
            'scanning_files': this.searchOnlyNewFilesStr,
            'checking_missing_cats': this.checkOnlyExistCatsStr,
            'checking_missing_files': this.checkOnlyExistFilesStr,
            'completed': this.scanCompleteStr
        };
        
        const phaseLabel = phaseLabels[scanData.phase] || scanData.phase;
        if (scanData.completed) {
            phaseIndicator.style.display = 'none';
            phaseIndicator.innerHTML = '';
        } else {
            phaseIndicator.style.display = '';
            phaseIndicator.innerHTML = `<span class="phase-badge phase-${scanData.phase}">${phaseLabel}</span>`;
        }
        
        // Update status (two lines: status at the top, percentage + items at the bottom)
        const currentStatus = document.getElementById('currentStatus');
        
        if (scanData.completed) {
            currentStatus.innerHTML = '<strong style="color: #ffffff;">' + this.scanCompleteStr + '</strong>';
            progressPercent.style.color = '#ffffff';
            progressPercent.textContent = '100%';
        } else {
            currentStatus.innerHTML = `<span class="spinner me-2"></span>${scanData.current} / ${scanData.total} ${this.processedSuffixStr}`;
            progressPercent.textContent = progress + '%';
        }
        
        // Update stats
        if (scanData.stats) {
            switch (this.mode) {
                case 0:
                    // All modes
                    document.getElementById('stat-new-cats').textContent = scanData.stats.new_cats || 0;
                    document.getElementById('stat-new-downloads').textContent = scanData.stats.new_downloads || 0;
                    document.getElementById('stat-missing-cats').textContent = scanData.stats.missing_cats || 0;
                    document.getElementById('stat-missing-files').textContent = scanData.stats.missing_files || 0;
                    break;
                case 1:
                    // New Categories
                    document.getElementById('stat-new-cats').textContent = scanData.stats.new_cats || 0;
                    break;
                case 2:
                    // New Downloads
                    document.getElementById('stat-new-downloads').textContent = scanData.stats.new_downloads || 0;
                    break;
                case 3:
                    // Missing Categories
                    document.getElementById('stat-missing-cats').textContent = scanData.stats.missing_cats || 0;
                    break;
                case 4:
                    // Missing Files
                    document.getElementById('stat-missing-files').textContent = scanData.stats.missing_files || 0;
                    break;
            }            
        }
        
        // Update log (only add new entries, limit to last 100 for performance)
        if (scanData.log && scanData.log.length > 0) {
            const logContainer = document.getElementById('logContainer');
            const MAX_LOG_ENTRIES = 100;

            logContainer.innerHTML = '';
            
            // Re-render the last server-provided entries to keep modal and export in sync.
            const visibleLogEntries = scanData.log.slice(-MAX_LOG_ENTRIES);

            for (let i = 0; i < visibleLogEntries.length; i++) {
                const entry = document.createElement('div');
                entry.className = 'log-entry';
                
                // Color code log entries
                const logText = visibleLogEntries[i];
                if (logText.includes('✓')) {
                    entry.className += ' text-success';
                } else if (logText.includes('✗') || logText.includes('ERROR')) {
                    entry.className += ' text-danger';
                } else if (logText.includes('→')) {
                    entry.className += ' text-info';
                }
                
                entry.textContent = logText;
                logContainer.appendChild(entry);
            }
            
            // Auto-scroll to bottom
            logContainer.scrollTop = logContainer.scrollHeight;
        }
    },

    getOverallProgress(scanData) {
        if (scanData.completed || scanData.phase === 'completed') {
            return 100;
        }

        const phaseOrder = ['scanning_folders', 'scanning_files', 'checking_missing_cats', 'checking_missing_files'];
        const phaseIndex = phaseOrder.indexOf(scanData.phase);

        if (phaseIndex === -1) {
            return Math.round(scanData.progress || 0);
        }

        const localProgress = scanData.total > 0 ? Math.min(100, (scanData.current / scanData.total) * 100) : 0;

        return Math.round(((phaseIndex * 100) + localProgress) / phaseOrder.length);
    },
    
    onScanComplete(scanData) {
        clearInterval(this.pollInterval);
        this.scanFinished = true;
        this.processRequestInFlight = false;
        
        const progressBar = document.getElementById('progressBar');
        progressBar.classList.remove('progress-bar-animated', 'bg-info');
        progressBar.classList.add('bg-success');
        progressBar.style.width = '100%';
        progressBar.setAttribute('aria-valuenow', '100');
        
        document.getElementById('currentStatus').innerHTML = 
            '<strong style="color: #ffffff;">' + this.scanCompleteStr + '</strong>';
        document.getElementById('progressPercent').style.color = '#ffffff';
        document.getElementById('progressPercent').textContent = '100%';

        const phaseIndicator = document.getElementById('phaseIndicator');
        phaseIndicator.style.display = 'none';
        phaseIndicator.innerHTML = '';

        this.renderCompletionInfo(scanData);
        
        document.getElementById('btnReset').style.display = 'inline-block';
        document.getElementById('btnDownloadLog').style.display = 'inline-block';
        
        // Calculate total items found across all categories
        let totalItems = 0;
        if (scanData.stats) {
            totalItems = (scanData.stats.new_cats || 0) + 
                        (scanData.stats.new_downloads || 0) + 
                        (scanData.stats.missing_cats || 0) + 
                        (scanData.stats.missing_files || 0);
        }
        
        // Show hint only if more than 100 items were found
        if (totalItems > 100) {
            document.getElementById('btnDownloadLogHint').style.display = 'inline-block';
        }
        
        // Show "Run Real" button if this was a test run and changes were found
        if (this.testrun && scanData.stats) {
            const hasChanges = (scanData.stats.new_cats > 0 || 
                               scanData.stats.new_downloads > 0 ||
                               scanData.stats.missing_cats > 0 ||
                               scanData.stats.missing_files > 0);
            const btnRunReal = document.getElementById('btnRunReal');
            if (btnRunReal && hasChanges) {
                btnRunReal.style.display = 'inline-block';
            }
        }
    },

    renderCompletionInfo(scanData) {
        const infoBox = document.getElementById('scanSummaryInfo');

        if (!infoBox) {
            return;
        }

        const infoParts = [];

        if (scanData.duration_message) {
            infoParts.push(`<div>${this.escapeHtml(scanData.duration_message)}</div>`);
        }

        if (scanData.debug_info_lines && scanData.debug_info_lines.length > 0) {
            infoParts.push(`<div class="mt-2">${scanData.debug_info_lines.map((line) => this.escapeHtml(line)).join('<br>')}</div>`);
        }

        if (this.cronInfoHtml) {
            infoParts.push(`<div class="mt-2">${this.cronInfoHtml}</div>`);
        }

        if (infoParts.length === 0) {
            infoBox.style.display = 'none';
            infoBox.innerHTML = '';
            return;
        }

        infoBox.innerHTML = infoParts.join('');
        infoBox.style.display = 'block';
    },

    escapeHtml(value) {
        const wrapper = document.createElement('div');
        wrapper.textContent = value;
        return wrapper.innerHTML;
    },
    
    async resetScan() {
        await fetch(`${this.workerUrl}?action=reset&key=${this.secret}`);
        location.reload();
    },
    
    addLog(message, type = 'info') {
        const logContainer = document.getElementById('logContainer');
        const entry = document.createElement('div');
        entry.className = 'log-entry text-' + type;
        entry.textContent = message;
        logContainer.appendChild(entry);
        logContainer.scrollTop = logContainer.scrollHeight;
    }
};

scanMonitor.init();
</script>

</body>
</html>