/**
 * Tasks Manager - Tasks Page UI Management
 * Handles the tasks page display, filtering, and UI interactions
 * Uses individual task operations from the task/ folder
 */
class TasksManager {
  constructor() {
    this.tasks = [];
    this.currentCategory = 'all';
    this.highlightedTaskId = null;
    this.taskManager = new TaskManager();
    this.init();
    
    // Start global live log updater
    this.startGlobalLiveLogUpdater();
    
    this.refreshInterval = null;
    this.activeLogStreams = new Map(); // Track active log streams
    this.autoRefreshIntervals = new Map(); // Track auto-refresh intervals for running tasks
    
    // Initialize modular components for individual task operations (only if available)
    try {
      this.commands = new TaskCommands();
      this.actions = new TaskActions(this, this.commands);
      this.router = new TaskRouter(this, this.actions);
      this.taskManager = new TaskManager();
    } catch (error) {
      console.warn('⚠️ Task system components not yet loaded, will initialize later:', error.message);
      this.commands = null;
      this.actions = null;
      this.router = null;
      this.taskManager = null;
    }
    
    // Initialize from URL parameters
    this.initializeFromURL();
    
    // Load tasks and setup (only if task system is available)
    if (this.commands) {
      this.loadTasks();
    } else {
      //// // console.log('⏳ Task system will be initialized later');
    }
    
    // Setup global functions
    this.setupGlobalFunctions();

    // Subscribe to the SSE bus once for the page so every visible task row
    // reacts to status changes, not just ones spawned in this session.
    this.setupTaskBusListeners();

    // Setup mobile menu
    this.setupMobileMenu();
    
    //// // console.log('✅ TasksManager initialized with modular architecture');
  }

  // Initialize task system after scripts are loaded
  initializeTaskSystem() {
    try {
      //// // console.log('🔧 Initializing task system components...');
      
      // Check if TaskManager is available
      if (typeof TaskManager === 'undefined') {
        console.warn('⚠️ TaskManager not available yet, deferring initialization');
        return false;
      }
      
      this.commands = new TaskCommands();
      this.actions = new TaskActions(this, this.commands);
      this.router = new TaskRouter(this, this.actions);
      this.taskManager = new TaskManager(); // Add TaskManager for task operations
      
      // Now load tasks since system is ready
      this.loadTasks();
      
      //// // console.log('✅ Task system initialized successfully');
      return true;
    } catch (error) {
      console.error('❌ Failed to initialize task system:', error);
      return false;
    }
  }

  // Main initialization method for the tasks page
  async init() {
    //// // console.log('🔧 Initializing TasksManager...');
    
    // Initialize task system if not already done
    if (!this.taskManager) {
      let initialized = false;
      let attempts = 0;
      const maxAttempts = 5;
      
      while (!initialized && attempts < maxAttempts) {
        //// // console.log(`🔄 Attempting to initialize task system (${attempts + 1}/${maxAttempts})...`);
        initialized = this.initializeTaskSystem();
        
        if (!initialized) {
          attempts++;
          await new Promise(resolve => setTimeout(resolve, 200)); // Wait 200ms
        }
      }
      
      if (!initialized) {
        console.warn('⚠️ Task system initialization failed after retries');
      }
    }
    
    // Setup refresh interval
    this.setupRefreshInterval();
    
    // Reconstructor() {
    this.tasks = [];
    this.taskManager = new TaskManager();
    this.activeLogStreams = new Map(); // Track active log streams
    // console.log('🔍 TasksManager initialized');
  }

  initializeFromURL() {
    const currentUrl = new URL(window.location.href);
    const searchParams = currentUrl.searchParams;
    
    // Check if we're on the main tasks page (not app page)
    const isMainTasksPage = currentUrl.pathname === '/tasks' || currentUrl.pathname.startsWith('/tasks/') || currentUrl.pathname === '/tasks.html';

    if (isMainTasksPage) {
      // Category from the path (/tasks/<category>), else legacy ?=<category>.
      const seg = currentUrl.pathname.replace(/^\/tasks\/?/, '').split('/')[0];
      this.currentCategory = seg || searchParams.get('') || 'all';
      
      // Only check for specific task parameter if we're not coming from an app page
      const taskParam = searchParams.get('task');
      if (taskParam) {
        // console.log(`🎯 Found task parameter in URL: ${taskParam} on main tasks page`);
        this.highlightedTaskId = taskParam;
      } else {
        // Clear any existing highlighted task when on main tasks page without task param
        this.highlightedTaskId = null;
        // console.log(`🎯 Clearing highlighted task on main tasks page`);
      }
    } else {
      // Not on main tasks page, get default filter from localStorage
      this.currentCategory = localStorage.getItem('tasksDefaultFilter') || 'all';
      this.highlightedTaskId = null; // Always clear when not on tasks page
    }
    
    // console.log(`🎯 Tasks category from URL: ${this.currentCategory}`);
  }

  updateURL(category, taskId = null) {
    // Update URL without page reload and without hash
    let newURL = `/tasks/${category || 'all'}`;
    if (taskId) {
      newURL += `?task=${taskId}`;
    }
    
    // Prevent the SPA from interfering
    if (window.librePortalSPA) {
      window.librePortalSPA.currentRoute = newURL;
    }
    
    // Use a timeout to avoid conflicts with SPA routing
    setTimeout(() => {
      window.history.pushState({ category, taskId }, '', newURL);
    }, 0);
  }

  async init() {
    //// // console.log('🔧 Initializing TasksManager...');
    
    // Load initial tasks and refresh sidebar counts
    await this.loadTasks();
    
    // Force a refresh to ensure latest data
    // console.log('🔄 Refreshing tasks data on initialization...');
    await this.loadTasks();
    
    // Setup auto-refresh
    this.setupAutoRefresh();
    
    // Setup global functions
    this.setupGlobalFunctions();

    // Subscribe to the SSE bus once for the page so every visible task row
    // reacts to status changes, not just ones spawned in this session.
    this.setupTaskBusListeners();

    // Setup mobile menu
    this.setupMobileMenu();
    
    //// // console.log('✅ TasksManager initialized');
  }

  async refreshTasks() {
    // Show refresh notification
    const refreshNotification = window.notificationSystem.info(
      '🔄 Refreshing tasks...', 
      'Tasks', 
      null, 
      null
    );
    
    try {
      await this.loadTasks();
      
      // Remove refresh notification and show success
      if (refreshNotification && refreshNotification.remove) {
        refreshNotification.remove();
      }
      
      if (window.notificationSystem) {
        window.notificationSystem.success(
          '🔄 Tasks refreshed successfully',
          'Tasks',
          null,
          null
        );
      }
    } catch (error) {
      console.error('Error refreshing tasks:', error);
      
      // Remove refresh notification and show error
      if (refreshNotification && refreshNotification.remove) {
        refreshNotification.remove();
      }
      
      if (window.notificationSystem) {
        window.notificationSystem.error(
          `⚠️ Failed to refresh tasks: ${error.message}`,
          'Tasks',
          null,
          null
        );
      }
    }
  }

  async loadTasks() {
    try {
      //// // console.log('🔄 Loading tasks from file system...');
      
      // Check if task system is available
      if (!this.taskManager) {
        console.warn('⚠️ Task system not yet initialized, skipping task loading');
        this.tasks = [];
        return;
      }
      
      // Get tasks using new system
      // console.log('📥 Getting tasks using new queue system...');
      
      // Get queue and current status
      let queue = [];
      let current = {};
      
      try {
        const queueResponse = await fetch('/read-file?path=tasks/queue.json');
        if (queueResponse.ok) {
          const queueText = await queueResponse.text();
          if (queueText.trim()) {  // Only parse if not empty
            try {
              queue = JSON.parse(queueText);
            } catch (parseError) {
              console.warn('⚠️ Invalid queue.json format, starting with empty queue');
              queue = [];
            }
          }
        }
      } catch (error) {
        // console.log('📝 Queue file not found, starting with empty queue');
      }
      
      try {
        const currentResponse = await fetch('/read-file?path=tasks/current.json');
        if (currentResponse.ok) {
          const currentText = await currentResponse.text();
          if (currentText.trim()) {  // Only parse if not empty
            try {
              current = JSON.parse(currentText);
            } catch (parseError) {
              console.warn('⚠️ Invalid current.json format, treating as empty');
              current = {};
            }
          }
        }
      } catch (error) {
        // console.log('📝 Current file not found, no current task');
      }
      
      // Load individual task files
      const allTasks = [];
      
      // Add queued tasks
      for (const taskId of queue) {
        try {
          const task = await this.taskManager.getTask(taskId);
          if (task) allTasks.push(task);
        } catch (error) {
          console.warn(`⚠️ Failed to load queued task ${taskId}:`, error);
        }
      }
      
      // Add current task if different from queue
      if (current.id && !queue.includes(current.id)) {
        try {
          const task = await this.taskManager.getTask(current.id);
          if (task) allTasks.push(task);
        } catch (error) {
          console.warn(`⚠️ Failed to load current task ${current.id}:`, error);
        }
      }
      
      // Scan tasks folder for all task files (including completed ones) - OPTIMIZED
      try {
        // console.log('🔍 Scanning tasks folder for all task files...');
        const tasksResponse = await fetch('/read-directory?path=tasks');
        if (tasksResponse.ok) {
          const files = await tasksResponse.json();
          const taskFiles = files.filter(file => 
            file.endsWith('.json') && 
            file !== 'queue.json' && 
            file !== 'current.json'
          );
          
          // console.log(`📁 Found ${taskFiles.length} task files in folder`);
          
          // OPTIMIZATION: Batch load tasks instead of individual calls
          const missingTaskIds = taskFiles
            .map(file => file.replace('.json', ''))
            .filter(taskId => !allTasks.find(task => task.id === taskId));
          
          if (missingTaskIds.length > 0) {
            // console.log(`📦 Batch loading ${missingTaskIds.length} missing tasks...`);
            try {
              const batchResponse = await fetch('/read-tasks-batch', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ taskIds: missingTaskIds })
              });
              
              if (batchResponse.ok) {
                const batchTasks = await batchResponse.json();
                batchTasks.forEach(task => {
                  if (task) {
                    allTasks.push(task);
                    // console.log(`✅ Added completed task ${task.id} from batch load`);
                  }
                });
              } else {
                // Fallback to individual loading if batch endpoint not available
                // console.log('⚠️ Batch endpoint not available, falling back to individual loading');
                await this.loadTasksIndividually(missingTaskIds, allTasks);
              }
            } catch (error) {
              console.warn('⚠️ Batch loading failed, falling back to individual loading:', error);
              await this.loadTasksIndividually(missingTaskIds, allTasks);
            }
          }
        }
      } catch (error) {
        console.warn('⚠️ Failed to scan tasks folder:', error);
      }
      
      //// // console.log('📊 Task counts:', {
        //queued: queuedTasks.length,
        //processing: processingTasks.length,
        //completed: completedTasks.length
      //});
      
      // Combine all tasks
      this.tasks = allTasks;
      
      // Sort by creation time (newest first)
      this.tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
      
      // console.log(`✅ Loaded ${this.tasks.length} tasks`);
      //// // console.log('📋 All tasks:', this.tasks);
      
      this.renderTasks();
      this.updateStats();
      this.updateSidebarCounts();
      this.generateAppCategories();
    } catch (error) {
      console.error('❌ Failed to load tasks:', error);
      if (window.notificationSystem) {
        window.notificationSystem.error(`Failed to load tasks: ${error.message}`);
      }
      this.tasks = [];
      this.renderTasks();
      this.updateStats();
      this.updateSidebarCounts();
      this.generateAppCategories();
    }
  }

  async loadTasksIndividually(taskIds, allTasks) {
    // Fallback method for individual task loading
    for (const taskId of taskIds) {
      try {
        const task = await this.taskManager.getTask(taskId);
        if (task) {
          allTasks.push(task);
          // console.log(`✅ Added completed task ${taskId} from individual load`);
        }
      } catch (error) {
        console.warn(`⚠️ Failed to load task ${taskId}:`, error);
      }
    }
  }

  renderTasks() {
    const container = document.getElementById('tasks-list');
    if (!container) return;

    // Capture which task panels are currently open and where the user
    // is scrolled to *before* the innerHTML rebuild. Without this,
    // every refresh slams every expanded log shut and snaps to the top
    // — which makes watching a live task feel hostile.
    const expandedIds = new Set();
    container.querySelectorAll('.task-details.task-details-open').forEach(el => {
        const taskId = (el.id || '').replace(/^details-/, '');
        if (taskId) expandedIds.add(taskId);
    });
    const scrollParent = container.closest('.main') || document.scrollingElement || document.documentElement;
    const savedScrollTop = scrollParent ? scrollParent.scrollTop : window.scrollY;

    // Filter tasks based on current category and specific task
    let filteredTasks = this.filterTasksByCategory(this.tasks, this.currentCategory);
    
    // Note: Don't filter out other tasks when one is highlighted
    // highlightedTaskId is only for auto-expansion, not for filtering

    if (filteredTasks.length === 0) {
      let message;
      if (this.highlightedTaskId) {
        message = `Task ${this.highlightedTaskId} not found`;
      } else {
        const categoryName = this.getCategoryDisplayName(this.currentCategory);
        message = `No ${categoryName.toLowerCase()} tasks found`;
      }
      
      container.innerHTML = `
        <div class="task-item">
          <div class="task-header">
            <div class="task-info">
              <span class="task-status status-completed">info</span>
              <span class="task-command">$ ${message}</span>
              <span class="task-time">just now</span>
            </div>
            <div class="task-actions">
            </div>
          </div>
          
          <div class="task-details task-details-open">
            <div class="task-output">Run a task or install an application to see your task list here</div>
          </div>
        </div>
      `;
      return;
    }

    // Sort tasks by creation time (newest first)
    const sortedTasks = filteredTasks.sort((a, b) => 
      new Date(b.createdAt) - new Date(a.createdAt)
    );

    const html = sortedTasks.map(task => this.renderTask(task)).join('');
    container.innerHTML = html;

    // Re-open everything that was open before the rebuild and re-attach
    // log streaming so live tasks keep updating without an extra click.
    expandedIds.forEach(taskId => {
        const details = document.getElementById(`details-${taskId}`);
        if (!details) return;
        details.style.display = 'block';
        details.classList.add('task-details-open');
        const btn = document.querySelector(`.task-btn.toggle-details[onclick*="toggleTaskDetails('${taskId}')"]`);
        if (btn) btn.classList.add('expanded');

        const t = this.tasks.find(x => x.id === taskId);
        if (t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending')) {
            if (typeof this.startLogStreaming === 'function') this.startLogStreaming(taskId, t);
        } else {
            if (typeof this.loadTaskLogs === 'function') this.loadTaskLogs(taskId);
        }
    });

    // Restore scroll. Defer one frame so the browser has laid out the
    // new content height before we scroll back into it.
    if (scrollParent) {
        requestAnimationFrame(() => { scrollParent.scrollTop = savedScrollTop; });
    }
  }

  filterTasksByCategory(tasks, category) {
    switch (category) {
      case 'all':
        return tasks;
      
      case 'queued':
      case 'running':
      case 'completed':
      case 'failed':
        return tasks.filter(task => task.status === category);
      
      case 'install':
      case 'uninstall':
        return tasks.filter(task => task.type === category);
      
      case 'management':
        return tasks.filter(task => 
          ['restart', 'start', 'stop'].includes(task.type)
        );
      
      case 'backup':
        return tasks.filter(task => 
          ['backup', 'restore', 'delete'].includes(task.type)
        );
      
      case 'config':
        return tasks.filter(task => task.type === 'update_config');
      
      default:
        // Assume it's an app name
        return tasks.filter(task => task.app === category);
    }
  }

  getCategoryDisplayName(category) {
    const displayNames = {
      'all': 'All Tasks',
      'queued': 'Queued',
      'running': 'Running',
      'completed': 'Completed',
      'failed': 'Failed',
      'install': 'Install',
      'uninstall': 'Uninstall',
      'management': 'Management',
      'backup': 'Backups',
      'config': 'Configuration',
      'libreportal': 'LibrePortal'
    };
    
    return displayNames[category] || category.charAt(0).toUpperCase() + category.slice(1);
  }

  renderTask(task) {
    // console.log(`🔍 renderTask called with task:`, task);
    
    // Debug undefined status
    if (!task.status) {
      console.warn(`⚠️ Task ${task.id} has undefined status:`, task);
    }
    
    const statusClass = `status-${task.status || 'unknown'}`;
    const timeAgo = this.getTimeAgo(task.createdAt);
    const isRunning = task.status === 'running';
    const isFailed = task.status === 'failed';
    const hasOutput = task.output && task.output.length > 0;
    const hasError = task.error && task.error.length > 0;
    const hasLogs = task.log && Array.isArray(task.log) && task.log.length > 0;
    
    // console.log(`🔍 Task fields check:`, {
      //hasOutput: hasOutput,
      //hasError: hasError, 
      //hasLogs: hasLogs,
      //isRunning: isRunning,
      //outputLength: task.output ? task.output.length : 0,
      //error: task.error,
      //logCount: task.log ? task.log.length : 0
    //});
    
    const executionTime = task.startedAt && task.completedAt ? 
      this.calculateExecutionTime(task.startedAt, task.completedAt) : null;
    
    // console.log('🔍 renderTask debug:', {
      //taskStatus: task.status,
      //statusClass: statusClass,
      //statusDisplay: task.status ? task.status.toUpperCase() : 'UNKNOWN'
    //});

    return `
      <div class="task-item" data-task-id="${task.id}">
        <div class="task-header" onclick="toggleTaskDetails('${task.id}')">
          <div class="task-info">
            ${this.renderTaskIcons(task)}
            <span class="task-title">${this.formatCommandForUser(task)}</span>
            <span class="task-status ${statusClass}">${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}</span>
            <span class="task-time">${timeAgo}</span>
            ${executionTime ? `<span class="task-duration">⏱️ ${executionTime}</span>` : ''}
          </div>
          <div class="task-actions">
            ${isFailed ? `
              <button class="task-btn retry" onclick="event.stopPropagation(); retryTask('${task.id}')" title="Retry Task">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
                </svg>
                Retry
              </button>
            ` : ''}
            <button class="task-btn toggle-details" onclick="event.stopPropagation(); toggleTaskDetails('${task.id}')" title="Toggle Task Details">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <polyline points="6,9 12,15 18,9"></polyline>
              </svg>
              <span class="task-btn-label">Logs</span>
            </button>
            <button class="task-btn delete" onclick="event.stopPropagation(); deleteTask('${task.id}')" title="Delete Task">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <line x1="18" y1="6" x2="6" y2="18"></line>
                <line x1="6" y1="6" x2="18" y2="18"></line>
              </svg>
              <span class="task-btn-label">Delete</span>
            </button>
          </div>
        </div>
        
        <!-- Simplified details section (metadata and logs only) -->
        <div class="task-details" id="details-${task.id}">
          <!-- Task metadata -->
          <div class="task-meta">
            <div class="meta-item">
              <strong>Task ID:</strong> <a href="/tasks/all?task=${task.id}" class="task-id-link" data-task-id="${task.id}">${task.id}</a>
            </div>
            <div class="meta-item">
              <strong>Type:</strong> ${task.type || 'unknown'}
            </div>
            <div class="meta-item">
              <strong>App:</strong> ${task.app ? `<a href="/app/${task.app}" class="task-app-link" data-app-name="${task.app}">${task.app}</a>` : 'system'}
            </div>
            <div class="meta-item">
              <strong>Created:</strong> ${new Date(task.createdAt).toLocaleString()}
            </div>
            ${task.startedAt ? `
              <div class="meta-item">
                <strong>Started:</strong> ${new Date(task.startedAt).toLocaleString()}
              </div>
            ` : ''}
            ${task.completedAt ? `
              <div class="meta-item">
                <strong>Completed:</strong> ${new Date(task.completedAt).toLocaleString()}
              </div>
            ` : ''}
            ${executionTime ? `
              <div class="meta-item">
                <strong>Execution Time:</strong> ${executionTime}
              </div>
            ` : ''}
          </div>
          
          <!-- Logs section only -->
          <div class="task-logs">
            <div class="log-container terminal-style" id="logs-${task.id}" style="height: 200px; overflow-y: auto; position: relative;">
              ${hasLogs ?
                task.log.map(log => `<div class="log-entry">${this.taskManager.parseAnsiColors(log)}</div>`).join('') :
                '<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(10, 18, 36, 0.85); display: flex; align-items: center; justify-content: center; z-index: 10;"><div class="loading-spinner" style="width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.18); border-top: 2px solid #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px;"></div>Loading logs...</div></div>'
              }
            </div>
          </div>
        </div>
      </div>
    `;
  }

  getStatusIcon(status) {
    const icons = {
      queued: '⏳',
      running: '🔄',
      completed: '✅',
      failed: '❌'
    };
    return icons[status] || '📋';
  }

  formatDuration(seconds) {
    if (seconds < 60) {
      return `${seconds}s`;
    } else if (seconds < 3600) {
      return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
    } else {
      const hours = Math.floor(seconds / 3600);
      const minutes = Math.floor((seconds % 3600) / 60);
      return `${hours}h ${minutes}m`;
    }
  }

  calculateExecutionTime(startedAt, completedAt) {
    if (!startedAt || !completedAt) return null;
    
    const start = new Date(startedAt);
    const end = new Date(completedAt);
    const durationMs = end - start;
    const durationSeconds = Math.floor(durationMs / 1000);
    
    return this.formatDuration(durationSeconds);
  }

  extractTimestamp(logEntry) {
    const match = logEntry.match(/\[([^\]]+)\]/);
    return match ? match[1] : '';
  }

  extractLogMessage(logEntry) {
    const match = logEntry.match(/\] (.+)$/);
    return match ? match[1] : logEntry;
  }

  async viewTaskLogs(taskId) {
    // Open logs in a modal or new view
    const task = this.tasks.find(t => t.id === taskId);
    if (!task) return;

    // Load full logs for modal
    const fullLogs = await this.taskManager.readFullTaskLog(taskId);

    // Create modal with streaming logs
    const modal = document.createElement('div');
    modal.className = 'task-logs-modal';
    modal.innerHTML = `
      <div class="modal-overlay" onclick="closeTaskLogsModal()"></div>
      <div class="modal-content">
        <div class="modal-header">
          <h3>📋 Task Logs - ${task.id}</h3>
          <div class="log-status">
            <span class="status-indicator" id="log-status-${taskId}">Loading...</span>
            <button class="log-toggle" id="log-toggle-${taskId}" onclick="toggleLogStreaming('${taskId}')">⏸️ Pause</button>
          </div>
          <button class="modal-close" onclick="closeTaskLogsModal()">×</button>
        </div>
        <div class="modal-body">
          <div class="task-info-summary">
            <div class="info-row">
              <strong>Command:</strong> <code>${task.command}</code>
            </div>
            <div class="info-row">
              <strong>Status:</strong> <span class="status-${task.status || 'unknown'}">${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}</span>
            </div>
            <div class="info-row">
              <strong>Created:</strong> ${new Date(task.createdAt).toLocaleString()}
            </div>
          </div>
          
          <div class="logs-section">
            <h4>Execution Logs <span class="log-line-count">(0 lines)</span></h4>
            <div class="log-viewer terminal-style" id="log-viewer-${taskId}">
              ${fullLogs.split('\n').map(log => `<div class="log-line">${this.taskManager.parseAnsiColors(log)}</div>`).join('')}
            </div>
          </div>
          
          ${task.output ? `
            <div class="output-section">
              <h4>Command Output</h4>
              <pre class="output-viewer">${this.taskManager.parseAnsiColors(task.output)}</pre>
            </div>
          ` : ''}
          
          ${task.error ? `
            <div class="error-section">
              <h4>Error Details</h4>
              <pre class="error-viewer">${this.escapeHtml(task.error)}</pre>
            </div>
          ` : ''}
        </div>
      </div>
    `;
    
    document.body.appendChild(modal);
    
    // Update line count
    const lineCount = fullLogs.split('\n').length;
    const lineCountElement = modal.querySelector('.log-line-count');
    if (lineCountElement) {
      lineCountElement.textContent = `(${lineCount} lines)`;
    }
    
    // Start streaming if task is running
    if (task.status === 'running' || task.status === 'queued') {
      this.startLogStreaming(taskId, modal);
    }
    
    // Store streaming controller
    this.activeLogStreams = this.activeLogStreams || new Map();
  }

  startLogStreaming(taskId, modal) {
    const statusIndicator = document.getElementById(`log-status-${taskId}`);
    const toggleButton = document.getElementById(`log-toggle-${taskId}`);
    const logViewer = document.getElementById(`log-viewer-${taskId}`);
    const lineCountElement = modal.querySelector('.log-line-count');
    
    if (!logViewer) return;
    
    let lineCount = logViewer.children.length;
    
    const stream = this.taskManager.streamTaskLog(
      taskId,
      (newLines) => {
        // Add new lines to existing content
        const preElement = logViewer.querySelector('pre');
        let currentContent = preElement ? preElement.textContent : '';
        const separator = currentContent.trim() && !currentContent.includes('Waiting for logs...') ? '\n' : '';
        let cleanedContent = currentContent.replace('Waiting for logs...', '').trim();
        const newContent = cleanedContent + separator + newLines.join('\n');
        preElement.innerHTML = this.taskManager.parseAnsiColors(newContent);
        logViewer.appendChild(lineElement);
        lineCount++;
        if (lineCountElement) {
          lineCountElement.textContent = `(${lineCount} lines)`;
        }
        
        // Auto-scroll to bottom
        logViewer.scrollTop = logViewer.scrollHeight;
        
        // Update status
        if (statusIndicator) {
          statusIndicator.textContent = '🔴 Live';
          statusIndicator.className = 'status-indicator live';
        }
        
        // Remove new-line highlighting after a moment
        setTimeout(() => {
          const newLines = logViewer.querySelectorAll('.new-line');
          newLines.forEach(line => line.classList.remove('new-line'));
        }, 2000);
      },
      (error) => {
        console.error('Log streaming error:', error);
        if (statusIndicator) {
          statusIndicator.textContent = '❌ Error';
          statusIndicator.className = 'status-indicator error';
        }
      }
    );
    
    // Store stream controller
    this.activeLogStreams.set(taskId, { stream, modal, isPaused: false });
    
    // Update toggle button
    if (toggleButton) {
      toggleButton.textContent = '⏸️ Pause';
    }
  }

  toggleLogStreaming(taskId) {
    const streamData = this.activeLogStreams?.get(taskId);
    if (!streamData) return;
    
    const toggleButton = document.getElementById(`log-toggle-${taskId}`);
    const statusIndicator = document.getElementById(`log-status-${taskId}`);
    
    if (streamData.isPaused) {
      // Resume streaming
      streamData.isPaused = false;
      if (toggleButton) toggleButton.textContent = '⏸️ Pause';
      if (statusIndicator) {
        statusIndicator.textContent = '🔴 Live';
        statusIndicator.className = 'status-indicator live';
      }
    } else {
      // Pause streaming
      streamData.isPaused = true;
      if (toggleButton) toggleButton.textContent = '▶️ Resume';
      if (statusIndicator) {
        statusIndicator.textContent = '⏸️ Paused';
        statusIndicator.className = 'status-indicator paused';
      }
    }
  }

  formatCommandForUser(task) {
    if (!task.command) return 'Unknown Task';

    const displayName = (slug) => window.getAppDisplayName ? window.getAppDisplayName(slug) : (slug.charAt(0).toUpperCase() + slug.slice(1));

    if (/^libreportal setup config\b/.test(task.command))   return 'LibrePortal - Apply Configuration';
    if (/^libreportal setup finalize\b/.test(task.command)) return 'LibrePortal - Finalize Setup';
    if (/^libreportal setup apply\b/.test(task.command))    return 'LibrePortal - Setup Wizard';

    // Self-update actions (WebUI "Update now" / "Check for updates").
    if (/^libreportal update (apply|now)\b/.test(task.command)) return 'LibrePortal - Update';
    if (/^libreportal update check\b/.test(task.command))       return 'LibrePortal - Check for Updates';

    // Backup engine — per-app actions.
    const backupDeleteAllMatch = task.command.match(/libreportal backup app delete_all (\w+)/);
    if (backupDeleteAllMatch) return `${displayName(backupDeleteAllMatch[1])} - Delete All Backups`;

    const backupDeleteMatch = task.command.match(/libreportal backup app delete (\w+) (.+)/);
    if (backupDeleteMatch) return `${displayName(backupDeleteMatch[1])} - Delete Backup`;

    const backupCreateMatch = task.command.match(/^libreportal backup app create (\w+)/);
    if (backupCreateMatch) return `${displayName(backupCreateMatch[1])} - Create Backup`;

    const backupScheduleMatch = task.command.match(/^libreportal backup app schedule (\w+)/);
    if (backupScheduleMatch) return `${displayName(backupScheduleMatch[1])} - Scheduled Backup`;

    const backupListMatch = task.command.match(/^libreportal backup app list (\w+)/);
    if (backupListMatch) return `${displayName(backupListMatch[1])} - List Backups`;

    // Backup engine — restore actions.
    const restoreStartMatch = task.command.match(/^libreportal restore app start (\w+)/);
    if (restoreStartMatch) return `${displayName(restoreStartMatch[1])} - Restore Backup`;

    const restoreListMatch = task.command.match(/^libreportal restore app list (\w+)/);
    if (restoreListMatch) return `${displayName(restoreListMatch[1])} - List Backups`;

    const migrateAppMatch = task.command.match(/^libreportal restore migrate app (\w+)/);
    if (migrateAppMatch) return `${displayName(migrateAppMatch[1])} - Migrate from Host`;

    // Backup engine — system-level actions (no per-app target).
    if (/^libreportal backup all\b/.test(task.command))                      return 'LibrePortal - Backup All Apps';
    if (/^libreportal backup verify\b/.test(task.command))                   return 'LibrePortal - Verify Backups';
    if (/^libreportal backup location add\b/.test(task.command))             return 'LibrePortal - Add Backup Location';
    if (/^libreportal backup location remove\b/.test(task.command))          return 'LibrePortal - Remove Backup Location';
    if (/^libreportal backup location init\b/.test(task.command))            return 'LibrePortal - Initialise Backup Locations';
    if (/^libreportal backup location check\b/.test(task.command))           return 'LibrePortal - Check Backup Locations';
    if (/^libreportal backup location list\b/.test(task.command))            return 'LibrePortal - List Backup Locations';
    if (/^libreportal backup location stats\b/.test(task.command))           return 'LibrePortal - Backup Location Stats';
    if (/^libreportal restore migrate system\b/.test(task.command))          return 'LibrePortal - Migrate System';
    if (/^libreportal restore migrate discover\b/.test(task.command))        return 'LibrePortal - Discover Backups';
    if (/^libreportal restore first-run\b/.test(task.command))               return 'LibrePortal - First-Run Restore';

    // `libreportal app tool <app> <tool_id> ['<args>']` — show the
    // tool's friendly label instead of "Tool Application". Pull the
    // label from window.toolsCatalog if loaded; otherwise titlecase
    // the snake_case tool id.
    const toolMatch = task.command.match(/libreportal app tool (\S+) (\S+)/);
    if (toolMatch) {
      const appName = toolMatch[1];
      const toolId = toolMatch[2];
      let label = null;
      const cat = window.toolsCatalog;
      if (cat && cat.apps && cat.apps[appName] && Array.isArray(cat.apps[appName].tools)) {
        const t = cat.apps[appName].tools.find(x => x.id === toolId);
        if (t && t.label) label = t.label;
      }
      if (!label) {
        label = toolId.split('_').map(w => w ? w.charAt(0).toUpperCase() + w.slice(1) : '').join(' ');
      }
      return `${displayName(appName)} - ${label}`;
    }

    // Parse libreportal commands. Capture only the app name token — anything after
    // (e.g. config overrides like `CFG_FOO=bar|...`) is for the CLI, not the title.
    const libreportalMatch = task.command.match(/libreportal app (\w+) (\S+)/);
    if (libreportalMatch) {
      const action = libreportalMatch[1];
      const appName = libreportalMatch[2];

      const actionMap = {
        'install': 'Install Application',
        'uninstall': 'Uninstall Application',
        'restart': 'Restart Application',
        'start': 'Start Application',
        'stop': 'Stop Application',
        'update': 'Update Application',
        'rebuild': 'Rebuild Application',
        'delete': 'Delete Backup',
        'backup': 'Backup Application'
      };

      const formattedAction = actionMap[action] || `${action.charAt(0).toUpperCase() + action.slice(1)} Application`;
      return `${displayName(appName)} - ${formattedAction}`;
    }
    
    // Handle other common patterns
    if (task.command.includes('docker-compose')) {
      return 'Docker Compose Operation';
    }
    
    if (task.command.includes('docker')) {
      return 'Docker Operation';
    }
    
    // Return first 50 chars of command as fallback
    return task.command.length > 50 ? task.command.substring(0, 47) + '...' : task.command;
  }

  getTaskTypeIcon(task) {
    if (!task.type) return { icon: '⚙️', class: 'custom' };

    const iconMap = {
      'install': { icon: '✅', class: 'install' },
      'app-install': { icon: '✅', class: 'install' },
      'uninstall': { icon: '❌', class: 'uninstall' },
      'restart': { icon: '🔄', class: 'restart' },
      'start': { icon: '▶️', class: 'start' },
      'stop': { icon: '⏹️', class: 'stop' },
      'update': { icon: '⬆️', class: 'update' },
      'rebuild': { icon: '🔨', class: 'rebuild' },
      'backup': { icon: '💾', class: 'backup' },
      'restore': { icon: '📦', class: 'restore' },
      'delete': { icon: '🗑️', class: 'delete' },
      'delete_all': { icon: '🗑️', class: 'delete' },
      'setup-config': { icon: '🛠️', class: 'setup' },
      'setup-finalize': { icon: '🎉', class: 'setup' },
      'custom': { icon: '⚙️', class: 'custom' }
    };

    return iconMap[task.type] || iconMap['custom'];
  }

  /* Detect a task that's an LibrePortal system action (no specific app) so
     the row can show the LibrePortal logo instead of a blank icon slot. */
  isLibrePortalSystemTask(task) {
    if (!task || !task.command || task.app) return false;
    return /^libreportal (setup|backup\s+all|backup\s+verify|backup\s+location|backup\s+repo|restore\s+migrate\s+system|restore\s+migrate\s+discover|restore\s+first-run|webui|config|update)\b/.test(task.command);
  }

  /* Render the leading icon(s) on a task row:
       - Per-app task → emoji type icon + app icon
       - System task → emoji type icon + LibrePortal logo
       - Anything else → emoji type icon only
     Keeps the layout consistent across every row regardless of source. */
  renderTaskIcons(task) {
    const typeIcon = `<span class="task-type-icon">${this.getTaskTypeIcon(task).icon}</span>`;
    if (task.app) {
      const appIconPath = this.getAppIconPath(task);
      return `${typeIcon}<img src="${appIconPath}" alt="${task.app}" class="task-app-icon" onerror="this.style.display='none'">`;
    }
    if (this.isLibrePortalSystemTask(task)) {
      return `${typeIcon}<img src="/icons/libreportal.svg" alt="LibrePortal" class="task-app-icon">`;
    }
    return typeIcon;
  }

  getAppIconPath(task) {
    if (!task.app) return null;
    
    // Try to get icon from commands if available
    if (this.commands && this.commands.getAppData) {
      const appData = this.commands.getAppData(task.app);
      if (appData && appData.icon) {
        return appData.icon;
      }
    }
    
    // Default icon path
    return `/icons/apps/${task.app}.svg`;
  }

  updateStats() {
    const stats = {
      queued: this.tasks.filter(t => t.status === 'queued').length,
      running: this.tasks.filter(t => t.status === 'running').length,
      completed: this.tasks.filter(t => t.status === 'completed').length,
      failed: this.tasks.filter(t => t.status === 'failed').length
    };

    Object.keys(stats).forEach(status => {
      const element = document.getElementById(`${status}-count`);
      if (element) element.textContent = stats[status];
    });
  }

  updateSidebarCounts() {
    // Update all sidebar category counts
    const categories = ['all', 'queued', 'running', 'completed', 'failed', 'install', 'uninstall', 'management', 'backup', 'config'];
    
    categories.forEach(category => {
      const count = this.filterTasksByCategory(this.tasks, category).length;
      const element = document.getElementById(`count-${category}`);
      if (element) element.textContent = count;
    });

    // Update app-specific counts
    const apps = [...new Set(this.tasks.map(task => task.app).filter(Boolean))];
    apps.forEach(app => {
      const count = this.tasks.filter(task => task.app === app).length;
      const element = document.getElementById(`count-app-${app}`);
      if (element) element.textContent = count;
    });
  }

  async generateAppCategories() {
    const container = document.getElementById('app-categories');
    if (!container) return;

    try {
      // Show loading state
      container.innerHTML = '<div class="loading-categories"><div class="loading-spinner"></div><p style="color: #ffffff;">Loading apps...</p></div>';

      // Try to load apps data if not already available
      let appsData = window.apps || [];
      
      // If window.apps is not available, try to load it
      if (!appsData || appsData.length === 0) {
        try {
          const appsResponse = await fetch('/read-file?path=apps.json');
          if (appsResponse.ok) {
            const appsText = await appsResponse.text();
            if (appsText.trim()) {
              appsData = JSON.parse(appsText);
              window.apps = appsData; // Cache for future use
            }
          }
        } catch (error) {
          // console.log('Could not load apps.json, will use fallback');
        }
      }
      
      // Filter for installed apps only and extract slugs
      const installedApps = appsData
        .filter(app => app.installed === true || app.status === 'installed')
        .map(app => {
          // Extract slug from command like "libreportal app install adguard"
          const command = app.command || '';
          const slugMatch = command.match(/libreportal app install\s+(.+)$/);
          const slug = slugMatch ? slugMatch[1].trim() : '';
          
          // Extract title from "Title - Description" format
          const fullName = app.name || '';
          const title = fullName.split(' - ')[0].trim();
          
          return { slug, title };
        })
        .filter(app => app.slug && app.title);
      
      // If no installed apps found from apps data, fall back to task-based apps
      if (installedApps.length === 0) {
        // console.log('No installed apps found, using task-based app list');
        const taskApps = [...new Set(this.tasks.map(task => task.app).filter(Boolean))];
        
        if (taskApps.length === 0) {
          container.innerHTML = '<div class="no-apps" style="color: var(--text-secondary); font-size: 12px; padding: 8px;">No apps found</div>';
          return;
        }

        const appCategories = taskApps.map(app => {
          const taskCount = this.tasks.filter(task => task.app === app).length;
          const displayName = window.getAppDisplayName ? window.getAppDisplayName(app) : (app.charAt(0).toUpperCase() + app.slice(1));
          return `
            <a href="#" class="sidebar-item" data-category="${app}" onclick="filterTasksByCategory('${app}')">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
                <line x1="9" y1="9" x2="15" y2="9"></line>
                <line x1="9" y1="15" x2="15" y2="15"></line>
              </svg>
              ${displayName}
              <span class="task-count" id="count-app-${app}">${taskCount}</span>
            </a>
          `;
        }).join('');

        container.innerHTML = appCategories;
        return;
      }

      const appCategories = installedApps.map(app => {
        const taskCount = this.tasks.filter(task => task.app === app.slug).length;
        return `
          <a href="#" class="sidebar-item" data-category="${app.slug}" onclick="filterTasksByCategory('${app.slug}')">
            <div class="status-indicator status-installed"></div>
            ${app.title}
            <span class="task-count" id="count-app-${app.slug}">${taskCount}</span>
          </a>
        `;
      }).join('');

      container.innerHTML = appCategories;
    } catch (error) {
      console.error('Error generating app categories:', error);
      // Final fallback to task-based app list
      const apps = [...new Set(this.tasks.map(task => task.app).filter(Boolean))];
      
      if (apps.length === 0) {
        container.innerHTML = '<div class="no-apps" style="color: var(--text-secondary); font-size: 12px; padding: 8px;">No apps found</div>';
        return;
      }

      const appCategories = apps.map(app => {
        const taskCount = this.tasks.filter(task => task.app === app).length;
        const displayName = window.getAppDisplayName ? window.getAppDisplayName(app) : (app.charAt(0).toUpperCase() + app.slice(1));
        return `
          <a href="#" class="sidebar-item" data-category="${app}" onclick="filterTasksByCategory('${app}')">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
              <line x1="9" y1="9" x2="15" y2="9"></line>
              <line x1="9" y1="15" x2="15" y2="15"></line>
            </svg>
            ${displayName}
            <span class="task-count" id="count-app-${app}">${taskCount}</span>
          </a>
        `;
      }).join('');

      container.innerHTML = appCategories;
    }
  }

  setupAutoRefresh() {
    // Refresh every 5 seconds
    this.refreshInterval = setInterval(() => {
      this.loadTasks();
    }, 5000);
  }

  setupMobileMenu() {
    // Setup mobile menu toggle (if needed)
    const mobileOverlay = document.getElementById('mobile-overlay');
    const sidebar = document.getElementById('sidebar');
    
    if (mobileOverlay && sidebar) {
      mobileOverlay.addEventListener('click', () => {
        sidebar.classList.remove('mobile-open');
        mobileOverlay.classList.remove('active');
      });
    }
  }

  filterTasksByCategoryHandler(category) {
    this.currentCategory = category;
    
    // Clear specific task filter when switching categories
    this.highlightedTaskId = null;
    
    // Update URL
    this.updateURL(category);
    
    // Update sidebar active state
    document.querySelectorAll('.sidebar-item').forEach(item => {
      item.classList.toggle('active', item.dataset.category === category);
    });
    
    this.renderTasks();
  }

  setupAutoRefresh() {
    // Only refresh when tasks page is visible and every 30 seconds
    this.refreshInterval = setInterval(() => {
      // Only refresh if we're on the tasks page
      if (window.location.pathname === '/tasks' || window.location.hash.includes('tasks')) {
        this.loadTasks();
      }
    }, 30000); // 30 seconds instead of 5
  }
  
  setupGlobalFunctions() {
    // Make functions available globally for onclick handlers
    window.filterTasksByCategory = (category) => {
      event.preventDefault();
      this.filterTasksByCategoryHandler(category);
    };

    // Delegated click handler for the Task ID and App links rendered inside
    // each task row. Behaviour:
    //   * Task ID always lands on the global /tasks page with that task open.
    //     If we're already there, just push the URL and toggle the row open
    //     (no content reload); otherwise SPA-navigate to /tasks.
    //   * App link goes to that app's /app page on whatever its default tab
    //     is (we don't pin tab=… so the app's own logic picks one).
    if (!window.__taskMetaLinksBound) {
      window.__taskMetaLinksBound = true;
      document.addEventListener('click', (e) => {
        const link = e.target.closest('a.task-id-link, a.task-app-link');
        if (!link) return;
        const href = link.getAttribute('href');
        if (!href) return;
        e.preventDefault();
        e.stopPropagation();

        const navigate = (url) => {
          if (window.spaClean && typeof window.spaClean.navigate === 'function') {
            window.spaClean.navigate(url);
          } else {
            window.location.href = url;
          }
        };

        if (link.classList.contains('task-id-link')) {
          const taskId = link.dataset.taskId;
          const onTasksPage = window.location.pathname.startsWith('/tasks');
          if (onTasksPage && taskId && typeof window.toggleTaskDetails === 'function') {
            // Already on /tasks — soft-update the URL and open the row.
            window.history.pushState({}, '', href);
            if (window.tasksManager) window.tasksManager.highlightedTaskId = taskId;
            window.toggleTaskDetails(taskId);
          } else {
            // Coming from /app or anywhere else — go to the tasks page; its
            // initializeFromURL picks up `task=` and auto-expands.
            navigate(href);
          }
        } else {
          // App link — go to the app page on its default tab.
          //
          // `task-parameter-preserve.js` stashes any `task=…` from the
          // initial page URL into sessionStorage.pendingTaskId, and
          // app-tabbed-manager's init reads that as a fallback when the
          // current URL has no task param. Without clearing it here, an old
          // task id from /tasks would hijack the app page, force-switching
          // to the Tasks tab and opening that task. Clear it so the app
          // page lands on its actual default tab.
          try { sessionStorage.removeItem('pendingTaskId'); } catch {}
          navigate(href);
        }
      });
    }
    window.refreshTasks = () => this.refreshTasks();
    window.toggleTaskDetails = (taskId) => this.toggleTaskDetails(taskId);
    window.retryTask = (taskId) => this.retryTask(taskId);
    window.deleteTask = (taskId) => this.deleteTask(taskId);
    window.clearAllTasks = () => this.clearAllTasks();
    window.viewTaskLogs = (taskId) => this.viewTaskLogs(taskId);
    window.toggleLogStreaming = (taskId) => this.toggleLogStreaming(taskId);
    window.closeTaskLogsModal = () => {
      const modal = document.querySelector('.task-logs-modal');
      if (modal) {
        // Stop all active streaming for this modal
        this.activeLogStreams?.forEach((streamData, streamTaskId) => {
          if (streamData.modal === modal) {
            streamData.stream.stop();
            this.activeLogStreams.delete(streamTaskId);
          }
        });
        modal.remove();
      }
    };
    
    window.createAndExecuteTask = async (action, appName, config = '') => {
      try {
        // Create task using NEW signature
        const task = await this.taskManager.createTask(`libreportal app ${action} ${appName}`, action, appName, config);
        
        // Emit task creation event for AppTabbedManager
        window.dispatchEvent(new CustomEvent('taskCreated', {
          detail: { 
            taskId: task.id, 
            appName: appName, 
            action: action,
            timestamp: Date.now()
          }
        }));
        
        // Set up monitoring for this specific task
        this.monitorTask(task.id, appName, action);
        
        // Show success notification with app icon and direct link
        if (window.notificationSystem) {
          const taskUrl = `/tasks/all?task=${task.id}`;
          const typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon({ type: action })?.icon : '';
          const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
          window.notificationSystem.show(
            `Task created: ${action} ${appName}`,
            'info',
            appName,
            taskUrl,
            `/icons/apps/${appName}.svg`,
            customIcon
          );
        }
        
        return task;
      } catch (error) {
        console.error(`❌ Failed to create ${action} task for ${appName}:`, error);
        if (window.notificationSystem) {
          window.notificationSystem.error(`Failed to start ${action} task for ${appName}: ${error.message}`);
        }
        throw error;
      }
    };
  }

  // Single page-wide subscription to the TaskEventBus. This complements
  // monitorTask (which only fires for tasks created in this session) and
  // ensures any visible row updates when its status changes — including the
  // running -> completed/failed/cancelled transition that previously only
  // refreshed when the user switched tabs and came back.
  //
  // The guard is on `window`, not `this`, because TasksManager is constructed
  // in several places (system-loader, app-tabbed-manager, …). Without a
  // global guard each instance would attach its own listener and the user
  // would see N notifications for one task completion.
  setupTaskBusListeners() {
    if (window.__tasksManagerBusBound) return;
    window.__tasksManagerBusBound = true;

    const upsertLocal = (task) => {
      if (!task || !task.id) return;
      const idx = this.tasks.findIndex(t => t.id === task.id);
      if (idx >= 0) this.tasks[idx] = task; else this.tasks.unshift(task);
    };

    window.addEventListener('taskCreated', (e) => {
      const task = e.detail && e.detail.task;
      if (!task) return;
      upsertLocal(task);
      // Don't re-render the entire list here — `renderTasks` would blow away
      // any open dropdown's DOM. A targeted update is enough; the next
      // category switch / refresh will pull the new row in.
      this.updateTaskDisplay(task);
    });

    window.addEventListener('taskUpdated', (e) => {
      const task = e.detail && e.detail.task;
      if (!task) return;
      upsertLocal(task);
      if (task.status === 'running') {
        this.updateTaskStructure(task.id, task);
        this.startLogStreaming(task.id, task);
      }
      this.updateTaskDisplay(task);
    });

    window.addEventListener('taskCompleted', (e) => {
      const task = e.detail && e.detail.task;
      const taskId = (task && task.id) || (e.detail && e.detail.taskId);
      if (!taskId) return;
      if (task) upsertLocal(task);
      if (task) this.updateTaskDisplay(task);

      // Final render of any buffered log content, then stop the SSE listener.
      const stream = this.activeLogStreams && this.activeLogStreams.get(taskId);
      if (stream && typeof stream.render === 'function') stream.render();
      this.stopLogStreaming(taskId);

      // User-visible notification so they know without staring at the page.
      // Layout matches the "<App> task started!" format used by task-actions
      // and backup-manager so started/completed look like a matched pair.
      if (window.notificationSystem && task) {
        const appName = task.app || null;
        const action = task.type || 'task';
        const friendlyActionMap = {
          'install': 'Install', 'app-install': 'Install',
          'uninstall': 'Uninstall', 'restart': 'Restart',
          'start': 'Start', 'stop': 'Stop',
          'update': 'Update', 'rebuild': 'Rebuild',
          'backup': 'Backup', 'restore': 'Restore',
          'delete': 'Delete Backup', 'delete_all': 'Delete All Backups',
          'setup-config': 'Apply Configuration',
          'setup-finalize': 'Finalize Setup'
        };
        let actionTitle = friendlyActionMap[action] || (action.charAt(0).toUpperCase() + action.slice(1));
        // Tool tasks: override the generic "Tool" label with the tool's
        // friendly name (e.g. "Manage Shortcuts") so completion toasts
        // match what the user clicked.
        const toolCmdMatch = (task.command || '').match(/libreportal app tool (\S+) (\S+)/);
        if (toolCmdMatch) {
          const toolApp = toolCmdMatch[1];
          const toolId = toolCmdMatch[2];
          let toolLabel = null;
          const cat = window.toolsCatalog;
          if (cat && cat.apps && cat.apps[toolApp] && Array.isArray(cat.apps[toolApp].tools)) {
            const t = cat.apps[toolApp].tools.find(x => x.id === toolId);
            if (t && t.label) toolLabel = t.label;
          }
          if (!toolLabel) {
            toolLabel = toolId.split('_').map(w => w ? w.charAt(0).toUpperCase() + w.slice(1) : '').join(' ');
          }
          actionTitle = toolLabel;
        }
        const isSystemTask = action.startsWith('setup-');
        const displayName = isSystemTask
          ? 'LibrePortal'
          : ((appName && window.getAppDisplayName)
              ? window.getAppDisplayName(appName)
              : (appName || (task.command || `Task ${taskId}`)));
        const subjectLabel = isSystemTask ? 'System' : 'App';
        const onAppPage = window.location.pathname.startsWith('/app') && !window.location.pathname.startsWith('/apps');
        const url = (onAppPage && appName)
          ? `/app/${appName}?tab=tasks&task=${taskId}`
          : `/tasks/all?task=${taskId}`;
        const icon = appName ? `/icons/apps/${appName}.svg` : null;

        // Match the per-action emoji used in the task list rows (see
        // `getTaskTypeIcon`). Passed as the 6th `customIcon` arg so the
        // notification's leftmost icon slot shows the task *type* (install
        // ✅, backup 💾, restore 📦, …) instead of the generic level tick.
        const typeIcon = (this.getTaskTypeIcon ? this.getTaskTypeIcon(task) : null)?.icon || '';
        const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;

        let body;
        let level;
        if (task.status === 'completed') {
          body = `<strong>${subjectLabel}: ${displayName}</strong><br>${actionTitle} task completed!`;
          level = 'success';
        } else if (task.status === 'failed') {
          body = `<strong>${subjectLabel}: ${displayName}</strong><br>${actionTitle} task failed.`;
          level = 'error';
        } else if (task.status === 'cancelled') {
          body = `<strong>${subjectLabel}: ${displayName}</strong><br>${actionTitle} task cancelled.`;
          level = 'warning';
        }
        if (body) window.notificationSystem.show(body, level, appName, url, icon, customIcon);

        // Belt-and-braces: when the completion notification fires we also
        // tell the app-tabbed manager to re-enable that app's tabs and
        // buttons. The taskCompleted listener inside app-tabbed-manager
        // does this too, plus the 5s reconcile sweep — but routing it
        // through here as well means any one path being broken or
        // de-bound still leaves the user with a usable UI rather than
        // permanently-disabled tabs.
        if (appName && window.appTabbedManager && typeof window.appTabbedManager.enableAppButtons === 'function') {
          try { window.appTabbedManager.enableAppButtons(appName); } catch {}
        }
      }
    });

    window.addEventListener('taskDeleted', (e) => {
      const id = e.detail && e.detail.id;
      if (!id) return;
      this.tasks = this.tasks.filter(t => t.id !== id);
      const el = document.querySelector(`.task-item[data-task-id="${id}"]`);
      if (!el) return;
      const parent = el.parentElement;
      el.remove();
      if (!parent || parent.querySelector('.task-item')) return;
      if (parent.id === 'tasks-list') {
        this.renderTasks();
      } else if (parent.id === 'app-tasks') {
        const appName = (window.appTabbedManager && window.appTabbedManager.currentApp) || '';
        parent.innerHTML = `<p style="color: #888;">No tasks found for ${appName}.</p>`;
      }
    });
  }

  // Load task logs on demand
  async loadTaskLogs(taskId) {
    try {
      // console.log(`📋 Loading logs for task ${taskId}...`);

      // Show loading state
      const detailsElement = document.getElementById(`details-${taskId}`);
      if (detailsElement) {
        const logsContainer = detailsElement.querySelector('.task-logs');
        if (logsContainer) {
          logsContainer.innerHTML = '<div class="loading-logs">📋 Loading logs...</div>';
        }
      }
      
      // Load task data directly from task file
      const task = this.tasks.find(t => t.id === taskId);
      let output = '';
      
      if (task) {
        output = task.output || '';
      } else {
        // Try to fetch task data directly
        try {
          const response = await fetch(`/api/tasks/${taskId}`);
          if (response.ok) {
            const taskData = await response.json();
            output = taskData.output || '';
          }
        } catch (error) {
          console.warn('Failed to fetch task data:', error);
        }
      }
      
      if (output && output.trim().length > 0) {
        // Display the output
        const logsHtml = output.split('\n').map(log => `<div class="log-entry">${this.parseAnsiColors(log)}</div>`).join('');
        
        if (detailsElement) {
          const logsContainer = detailsElement.querySelector('.task-logs');
          if (logsContainer) {
            logsContainer.innerHTML = `
              <h4 style="margin-left: 20px;">📋 Execution Logs</h4>
              <div class="log-container terminal-style">${logsHtml}</div>
            `;
          }
        }
        
        // console.log(`✅ Loaded logs for task ${taskId}`);
      } else {
        // No logs available
        if (detailsElement) {
          const logsContainer = detailsElement.querySelector('.task-logs');
          if (logsContainer) {
            logsContainer.innerHTML = '<div class="no-logs">📋 No output available for this task.</div>';
          }
        }
        
        // console.log(`ℹ️ No logs available for task ${taskId}`);
      }
      
      return output;
    } catch (error) {
      console.error(`❌ Failed to load logs for task ${taskId}:`, error);
      if (window.notificationSystem) {
        window.notificationSystem.error(`Failed to load logs: ${error.message}`);
      }
      return '';
    }
  }

  // Monitor a specific task. State changes now arrive via SSE through
  // TaskEventBus, so this is mostly a hook for the UI to:
  //   - auto-expand the task if it's the one we just started
  //   - start log streaming when the task transitions to running
  //   - clean up local intervals when it terminates
  // No more polling; the only fetches happen on-demand via TaskManager.
  monitorTask(taskId, appName, action) {
    if (!this.highlightedTaskId || this.highlightedTaskId === taskId) {
      setTimeout(() => this.autoExpandTask(taskId), 1500);
    }

    let statusUpdateInterval = null;

    const onUpdate = (event) => {
      const t = event.detail && event.detail.task;
      if (!t || t.id !== taskId) return;

      if (t.status === 'running') {
        this.updateTaskStructure(taskId, t);
        this.startLogStreaming(taskId, t);
        if (this.highlightedTaskId === taskId && !statusUpdateInterval) {
          statusUpdateInterval = setInterval(() => this.updateHighlightedTaskStatus(taskId), 2000);
        }
      } else {
        this.updateTaskDisplay(t);
      }
    };

    const onComplete = (event) => {
      if (!event.detail || event.detail.taskId !== taskId) return;
      if (statusUpdateInterval) { clearInterval(statusUpdateInterval); statusUpdateInterval = null; }
      this.stopLogStreaming(taskId);
      window.removeEventListener('taskUpdated', onUpdate);
      window.removeEventListener('taskCompleted', onComplete);
    };

    window.addEventListener('taskUpdated', onUpdate);
    window.addEventListener('taskCompleted', onComplete);
  }

  // SSE-driven log streaming. Subscribes to `taskLog` events from the bus and
  // appends incoming chunks. Initial backlog is fetched once via the API.
  //
  // Resilient to DOM replacement: the logs container can be wiped out by
  // `renderTasks()` (the 30s auto-refresh) or by `loadTaskLogs()` (toggle
  // re-open). Instead of capturing a `preElement` reference once, `render()`
  // re-locates / re-creates it on every call from the cumulative `buffered`
  // string. Idempotent: if already streaming, a second call just re-renders.
  async startLogStreaming(taskId, task) {
    if (!this.activeLogStreams) this.activeLogStreams = new Map();

    if (this.activeLogStreams.has(taskId)) {
      const existing = this.activeLogStreams.get(taskId);
      if (existing && typeof existing.render === 'function') existing.render();
      return;
    }

    const state = { buffered: '' };

    const render = () => {
      const logsContainer = document.getElementById(`logs-${taskId}`);
      if (!logsContainer) return;
      const overlay = logsContainer.querySelector('div[style*="position: absolute"]');
      if (overlay) overlay.remove();
      let preElement = logsContainer.querySelector('pre.output-content');
      if (!preElement) {
        logsContainer.innerHTML = '';
        preElement = document.createElement('pre');
        preElement.className = 'output-content terminal-style';
        logsContainer.appendChild(preElement);
      }
      const atBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10;
      if (state.buffered) {
        preElement.innerHTML = this.taskManager.parseAnsiColors(state.buffered);
      } else {
        preElement.innerHTML = '<span style="color: #888;">Waiting for logs...</span>';
      }
      if (atBottom) logsContainer.scrollTop = logsContainer.scrollHeight;
    };

    // Initial backlog via the API.
    try {
      const initial = await this.taskManager.readFullTaskLog(taskId);
      if (initial && initial.length) state.buffered = initial;
    } catch { /* fall through to placeholder */ }
    render();

    const onLog = (event) => {
      const detail = event.detail || {};
      if (detail.id !== taskId || typeof detail.chunk !== 'string') return;
      state.buffered += detail.chunk;
      render();
    };
    window.addEventListener('taskLog', onLog);

    // SSE catch-up: when the backend restarts mid-task (e.g., libreportal
    // recreates itself during a CrowdSec install), the SSE event source
    // drops. EventSource auto-reconnects, but task.log events emitted
    // during the gap are lost. taskBusReady fires after every reconnect —
    // pull the missed bytes via the existing /:id/log?position=N endpoint
    // and splice them in. Skips on the initial connect (no gap).
    let initialReadyFired = false;
    const onBusReady = async () => {
      if (!initialReadyFired) { initialReadyFired = true; return; }
      try {
        const res = await fetch(`/api/tasks/${encodeURIComponent(taskId)}/log?position=${state.buffered.length}`);
        if (!res.ok) return;
        const missed = await res.text();
        if (missed) {
          state.buffered += missed;
          render();
        }
      } catch { /* network blip, next ready will retry */ }
    };
    window.addEventListener('taskBusReady', onBusReady);

    this.activeLogStreams.set(taskId, {
      stream: { stop: () => {
        window.removeEventListener('taskLog', onLog);
        window.removeEventListener('taskBusReady', onBusReady);
        this.activeLogStreams.delete(taskId);
      } },
      render,
      isPaused: false
    });
  }

  // Stop log streaming for a task
  stopLogStreaming(taskId) {
    if (this.activeLogStreams && this.activeLogStreams.has(taskId)) {
      const streamData = this.activeLogStreams.get(taskId);
      streamData.stream.stop();
      this.activeLogStreams.delete(taskId);
      // console.log(`⏹️ Stopped log streaming for task ${taskId}`);
    }
  }

  // Update task structure for live logs
  updateTaskStructure(taskId, task) {
    const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
    if (!taskElement) return;
    
    const detailsElement = taskElement.querySelector('.task-details');
    if (!detailsElement) return;
    
    // console.log(`🔄 Updating task structure for ${taskId} to show simplified logs`);
    
    // Check if logs container already exists
    const existingLogs = detailsElement.querySelector('.task-logs .log-container');
    if (existingLogs) {
      // console.log(`🔄 Logs container already exists for ${taskId}`);
      return; // Already exists, no need to update
    }
    
    // Add simplified logs section for running tasks
    if (task.status === 'running') {
      const logsHtml = `
        <div class="task-logs">
          <div class="log-container terminal-style" id="logs-${taskId}" style="height: 200px; overflow-y: auto; position: relative;">
            <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(10, 18, 36, 0.85); display: flex; align-items: center; justify-content: center; z-index: 10;"><div class="loading-spinner" style="width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.18); border-top: 2px solid #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px;"></div>Loading logs...</div></div>
          </div>
        </div>
      `;
      
      // Insert logs section at the bottom of details
      detailsElement.insertAdjacentHTML('beforeend', logsHtml);
      // console.log(`✅ Added simplified logs section for task ${taskId}`);
      
      // Auto-load logs
      this.loadTaskLogs(taskId);
    }
  }

  // Update task display in real-time
  updateTaskDisplay(task) {
    const taskElement = document.querySelector(`[data-task-id="${task.id}"]`);
    if (!taskElement) {
      // The monitored task isn't always rendered (different tab, list filtered out,
      // task already removed). Silently skip — this is the normal case.
      return;
    }
    
    // console.log(`🔄 Found task element:`, taskElement);
    
    // Update status and content
    const statusElement = taskElement.querySelector('.task-status');
    const contentElement = taskElement.querySelector('.task-content');
    
    if (statusElement) {
      const statusClass = `status-${task.status || 'unknown'}`;
      statusElement.className = `task-status ${statusClass}`;
      statusElement.innerHTML = `${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}`;
    } else {
      console.warn(`⚠️ Status element not found for task ${task.id}`);
    }

    // Mirror the status into the details panel's metadata block too — that
    // copy of the status was previously left stale until the page reloaded.
    const detailsStatus = taskElement.querySelector(`#details-${task.id} .task-meta .status-running, #details-${task.id} .task-meta .status-queued, #details-${task.id} .task-meta .status-pending, #details-${task.id} .task-meta .status-completed, #details-${task.id} .task-meta .status-failed, #details-${task.id} .task-meta .status-cancelled, #details-${task.id} .task-meta [class^="status-"]`);
    if (detailsStatus) {
      detailsStatus.className = `status-${task.status || 'unknown'}`;
      detailsStatus.innerHTML = `${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}`;
    }

    if (contentElement) {
      contentElement.textContent = task.command;
    }
    
    // console.log(`🔄 Updated task ${task.id} display: ${task.status}`);
  }

  // Start global live log updater - simple 2-second updates for all running tasks
  startGlobalLiveLogUpdater() {
    // console.log(`🔄 Starting global live log updater`);
    
    // Update every 2 seconds
    setInterval(async () => {
      // console.log(`🔄 Global updater running - checking tasks...`);
      
      // Find all running tasks
      const runningTasks = this.tasks.filter(task => task.status === 'running');
      // console.log(`🔄 Found ${runningTasks.length} running tasks:`, runningTasks.map(t => t.id));
      
      if (runningTasks.length > 0) {
        // console.log(`🔄 Updating live logs for ${runningTasks.length} running tasks`);
        
        // Update each running task's live logs
        for (const task of runningTasks) {
          // console.log(`🔄 About to update live logs for task ${task.id}`);
          await this.updateLiveLogsSimple(task.id);
        }
      } else {
        // console.log(`🔄 No running tasks found, skipping live log updates`);
      }
    }, 2000); // Every 2 seconds
  }

  // Simple live log update - no complex polling logic
  async updateLiveLogsSimple(taskId) {
    const liveLogsElement = document.getElementById(`live-logs-${taskId}`);
    if (!liveLogsElement) {
      // console.log(`⚠️ Live logs element not found for task ${taskId}`);
      return; // Silently skip if element not found
    }
    
    try {
      // console.log(`🔄 Reading log file for task ${taskId}`);
      
      // Read the log file content
      const response = await fetch(`/read-file?path=tasks/${taskId}.log`);
      // console.log(`🔄 Log file response status: ${response.status} for task ${taskId}`);
      
      if (response.ok) {
        const logContent = await response.text();
        // console.log(`🔄 Log file content length: ${logContent.length} chars for task ${taskId}`);
        // console.log(`🔄 First 100 chars of log content: "${logContent.substring(0, 100)}..."`);
        
        if (logContent.trim()) {
          // Split into lines and display
          const lines = logContent.split('\n').filter(line => line.trim());
          // console.log(`🔄 Displaying ${lines.length} log lines for task ${taskId}`);
          
          liveLogsElement.innerHTML = lines.map(line => 
            `<div class="log-entry">${this.parseAnsiColors(line)}</div>`
          ).join('');
          // Auto-scroll to bottom
          liveLogsElement.scrollTop = liveLogsElement.scrollHeight;
        } else {
          // console.log(`🔄 Log file is empty for task ${taskId}`);
          liveLogsElement.innerHTML = '<div class="log-entry">🔄 Waiting for logs...</div>';
        }
      } else {
        console.warn(`⚠️ Failed to read log file for task ${taskId}: ${response.status}`);
        liveLogsElement.innerHTML = '<div class="log-entry">⚠️ Unable to read logs</div>';
      }
    } catch (error) {
      // Silently handle errors
      console.warn(`⚠️ Error reading live logs for task ${taskId}:`, error);
      liveLogsElement.innerHTML = '<div class="log-entry">❌ Error loading logs</div>';
    }
  }

  // Load task logs automatically
  async loadTaskLogs(taskId) {
    try {
      const logsContainer = document.getElementById(`logs-${taskId}`);
      if (!logsContainer) {
        console.warn(`⚠️ Logs container not found for task ${taskId}`);
        return;
      }

      const task = this.tasks.find(t => t.id === taskId);
      const inMemoryLog = (task && Array.isArray(task.log) && task.log.length > 0) ? task.log : null;

      const renderInMemory = () => {
        if (!inMemoryLog) return false;
        logsContainer.innerHTML = inMemoryLog
          .map(line => `<div class="log-entry">${this.taskManager.parseAnsiColors(line)}</div>`)
          .join('');
        return true;
      };

      logsContainer.innerHTML = '<div class="log-entry">🔄 Loading logs...</div>';
      const isScrolledToBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10;

      const logResponse = await fetch(`/read-file?path=tasks/${taskId}.log`);
      if (logResponse.ok) {
        const logContent = await logResponse.text();
        if (logContent.trim()) {
          logsContainer.innerHTML = `<pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(logContent)}</pre>`;
          if (isScrolledToBottom) logsContainer.scrollTop = logsContainer.scrollHeight;
          return;
        }
      }

      if (renderInMemory()) return;

      logsContainer.innerHTML = '<div class="log-entry">ℹ️ No logs available for this task.</div>';

    } catch (error) {
      console.error(`❌ Error loading logs for task ${taskId}:`, error);
      const logsContainer = document.getElementById(`logs-${taskId}`);
      if (logsContainer) {
        logsContainer.innerHTML = '<div class="log-entry">❌ Failed to load logs.</div>';
      }
    }
  }

  // Load task output on demand
  async loadTaskOutput(taskId) {
    try {
      // Read the task file to get output
      const task = await this.taskManager.getTask(taskId);
      if (!task) return;

      const outputElement = document.querySelector(`[data-task-id="${taskId}"] .task-output`);
      if (!outputElement) return;

      // Show loading state
      outputElement.innerHTML = `
        <div class="loading-output">Loading output...</div>
      `;

      // Check if task has output
      if (task.output && task.output.trim()) {
        outputElement.innerHTML = `
          <h4>📤 Output</h4>
          <pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(task.output)}</pre>
        `;
      } else if (task.error && task.error.trim()) {
        outputElement.innerHTML = `
          <h4>❌ Error</h4>
          <pre class="error-content">${this.escapeHtml(task.error)}</pre>
        `;
      } else {
        // Try to read from log file
        const logResponse = await fetch(`/read-file?path=tasks/${taskId}.log`);
        if (logResponse.ok) {
          const logContent = await logResponse.text();
          if (logContent.trim()) {
            outputElement.innerHTML = `
              <pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(logContent)}</pre>
            `;
          } else {
            outputElement.innerHTML = `
              <h4>ℹ️ Information</h4>
              <div class="info-content">No output available for this task.</div>
            `;
          }
        } else {
          outputElement.innerHTML = `
            <h4>ℹ️ Information</h4>
            <div class="info-content">No output available for this task.</div>
          `;
        }
      }
    } catch (error) {
      console.error(`❌ Error loading task output for ${taskId}:`, error);
      const outputElement = document.querySelector(`[data-task-id="${taskId}"] .task-output`);
      if (outputElement) {
        outputElement.innerHTML = `
          <h4>❌ Error</h4>
          <div class="error-content">Failed to load task output: ${error.message}</div>
        `;
      }
    }
  }

  // Auto-expand a task when it's created
  async autoExpandTask(taskId) {
    // console.log(`🔄 Auto-expanding task ${taskId}`);
    
    // Wait for task to be rendered
    let attempts = 0;
    const maxAttempts = 10;
    
    const tryExpand = async () => {
      attempts++;
      // console.log(`🔄 Auto-expand attempt ${attempts}/${maxAttempts} for task ${taskId}`);
      
      // Check if task element exists
      const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
      if (!taskElement) {
        // console.log(`⚠️ Task element not found for ${taskId}, attempt ${attempts}`);
        if (attempts < maxAttempts) {
          setTimeout(tryExpand, 500); // Try again in 500ms
        } else {
          console.warn(`⚠️ Could not find task element for ${taskId} after ${maxAttempts} attempts`);
        }
        return;
      }
      
      // console.log(`✅ Found task element for ${taskId}`);
      
      // Get the details element
      const details = document.getElementById(`details-${taskId}`);
      if (!details) {
        // console.log(`⚠️ Details element not found for ${taskId}, attempt ${attempts}`);
        if (attempts < maxAttempts) {
          setTimeout(tryExpand, 500);
        } else {
          console.warn(`⚠️ Could not find details element for ${taskId}`);
        }
        return;
      }
      
      // console.log(`✅ Found details element for ${taskId}`);
      
      // Expand the task details
      details.style.display = 'block';
      details.classList.add('task-details-open');
      
      // Update toggle button
      const toggleBtn = document.querySelector(`.task-btn.toggle-details[onclick*="toggleTaskDetails('${taskId}')"]`);
      if (toggleBtn) {
        toggleBtn.classList.add('expanded');
      }
      
      // Scroll to the task
      taskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      
      // Load the output if task is completed
      const task = await this.taskManager.getTask(taskId);
      if (task && (task.status === 'completed' || task.status === 'failed')) {
        setTimeout(() => {
          this.loadTaskOutput(taskId);
        }, 1000);
      }
      
      // console.log(`✅ Auto-expanded task ${taskId}`);
    };
    
    tryExpand();
  }

  // Update highlighted task status and UI
  async updateHighlightedTaskStatus(taskId) {
    try {
      // Use lightweight summary for status updates
      const task = await this.taskManager.getTaskSummary(taskId);
      if (!task) return;
      
      // console.log(`🔄 Updating highlighted task ${taskId} status: ${task.status}`);
      
      // Update task display
      this.updateTaskDisplay(task);
      
      // If task completed or failed, always load output
      if ((task.status === 'completed' || task.status === 'failed')) {
        // console.log(`🔄 Task ${taskId} is ${task.status}, loading output...`);
        
        const details = document.getElementById(`details-${taskId}`);
        if (details && details.style.display === 'block') {
          // Load output regardless of current content
          this.loadTaskOutput(taskId);
        } else if (details) {
          // If details aren't open, mark for loading when opened
          details.setAttribute('data-load-output-on-open', 'true');
        }
      }
    } catch (error) {
      console.error(`❌ Error updating highlighted task status for ${taskId}:`, error);
    }
  }      

  toggleTaskDetails(taskId) {
    const details = document.getElementById(`details-${taskId}`);
    const toggleBtn = document.querySelector(`.task-btn.toggle-details[onclick*="toggleTaskDetails('${taskId}')"]`);
    
    if (details) {
      const isOpen = details.style.display === 'block';
      
      // Close all other task details and reset their buttons
      if (isOpen) {
        document.querySelectorAll('.task-details').forEach(otherDetails => {
          if (otherDetails.id !== `details-${taskId}`) {
            otherDetails.style.display = 'none';
            otherDetails.classList.remove('task-details-open');
          }
        });
        
        document.querySelectorAll('.task-btn.toggle-details').forEach(otherBtn => {
          if (!otherBtn.getAttribute('onclick').includes(taskId)) {
            otherBtn.classList.remove('expanded');
          }
        });
        
        // Close current
        details.style.display = 'none';
        details.classList.remove('task-details-open');
        if (toggleBtn) toggleBtn.classList.remove('expanded');
      } else {
        // Open current
        details.style.display = 'block';
        details.classList.add('task-details-open');
        if (toggleBtn) toggleBtn.classList.add('expanded');
        
        // Auto-load logs when opened. For active tasks, hand off to the live
        // streamer so SSE chunks keep updating the panel; for terminal tasks
        // a one-shot snapshot is enough.
        const t = this.tasks.find(x => x.id === taskId);
        if (t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending')) {
          this.startLogStreaming(taskId, t);
        } else {
          this.loadTaskLogs(taskId);
        }
        
        // Scroll to task
        const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
        if (taskElement) {
          taskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
      }
      
      // Update URL to include task parameter
      this.updateURL(this.currentCategory, isOpen ? null : taskId);
      this.highlightedTaskId = isOpen ? null : taskId;
    } else {
      // Remove task parameter from URL
      this.updateURL(this.currentCategory);
      this.highlightedTaskId = null;
    }
  }

  async retryTask(taskId) {
    if (!confirm('Are you sure you want to retry this task?')) return;

    try {
      const task = this.tasks.find(t => t.id === taskId);
      if (!task) {
        throw new Error('Task not found');
      }

      // Create a new task with the same command
      const newTask = await this.taskManager.createTask(
        task.command,
        task.type,
        task.app,
        task.config
      );
      
      //// // console.log(`✅ Task retried: ${newTask.id}`);
      
      // Refresh tasks to show the new one
      await this.loadTasks();
      
      if (window.notificationSystem) {
        // Use the source task's type icon — retrying a backup shows 💾 etc.
        const typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon(task)?.icon : '';
        const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
        window.notificationSystem.show('Task retried successfully', 'success', null, null, null, customIcon);
      }
    } catch (error) {
      console.error('Error retrying task:', error);
      if (window.notificationSystem) {
        window.notificationSystem.error(`Failed to retry task: ${error.message}`);
      }
    }
  }

  async deleteTask(taskId) {
    // Get the latest known status before deciding what to do. The server
    // refuses to delete tasks that are still running or queued (HTTP 409),
    // and we used to just propagate that as a red banner — but the user is
    // almost always trying to clean up a row that looked finished, or a
    // genuinely active task they want gone. Either way the right answer is
    // "cancel first, then delete", which is what we do here.
    const cached = (window.taskEventBus && window.taskEventBus.getTask)
      ? window.taskEventBus.getTask(taskId)
      : null;
    let task = cached || (this.tasks && this.tasks.find(t => t.id === taskId));
    if (!task && this.taskManager && this.taskManager.getTask) {
      try { task = await this.taskManager.getTask(taskId); } catch {}
    }

    const isActive = task && (task.status === 'running' || task.status === 'queued' || task.status === 'pending');

    const confirmed = await this._showDeleteTaskModal(task, isActive);
    if (!confirmed) return;

    try {
      if (isActive) {
        // Ask the server to cancel. POST /cancel either flips status straight
        // to `cancelled` (queued -> cancelled is synchronous) or drops a
        // `<id>.cancel` marker the bash processor picks up within ~1s
        // (running -> cancelled). We then wait for the terminal status via
        // SSE before issuing the delete.
        try { await this.taskManager.cancelTask(taskId); } catch {
          // If cancel itself failed (e.g. already terminal), fall through
          // and try the delete anyway.
        }
        await this._waitForTaskTerminal(taskId, 15_000);
      }

      // Try the polite delete first. If the task is still flagged as
      // running/queued server-side (e.g. the bash processor is dead and
      // never picked up the cancel marker), fall back to the force-delete
      // override so the user isn't stuck with a permanently-undeletable row.
      try {
        await this.taskManager.deleteTask(taskId);
      } catch (err) {
        const looks409 = /\bHTTP 409\b/.test(err && err.message ? err.message : '');
        if (!looks409) throw err;
        await this.taskManager.deleteTask(taskId, { force: true });
      }

      // Remove from local array and re-render
      this.tasks = this.tasks.filter(t => t.id !== taskId);
      this.renderTasks();
      this.updateStats();
      this.updateSidebarCounts();
      this.generateAppCategories();

      if (window.notificationSystem) {
        // Type icon from whatever the task was, with a trash fallback
        // because deletion is conceptually a 🗑️ action.
        const typeIcon = (task && this.getTaskTypeIcon ? this.getTaskTypeIcon(task)?.icon : '') || '🗑️';
        const customIcon = `<span style="font-size:18px;line-height:1;">${typeIcon}</span>`;
        window.notificationSystem.show('Task deleted successfully', 'info', null, null, null, customIcon);
      }
    } catch (error) {
      console.error('Error deleting task:', error);
      if (window.notificationSystem) {
        window.notificationSystem.error(`Failed to delete task: ${error.message}`);
      }
    }
  }

  // Confirmation modal for deleteTask. Uses the shared openEoModal so it
  // visually matches every other destructive confirmation on the app
  // (Uninstall, Apply Configuration, etc). Resolves true if the user
  // confirms Delete, false on Cancel / backdrop click / close.
  _showDeleteTaskModal(task, isActive) {
    return new Promise((resolve) => {
      const escHtml = (s) => String(s == null ? '' : s)
        .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

      const taskLabel = (task && (task.command || task.id)) || 'Unknown task';
      const taskStatus = (task && task.status) || 'unknown';

      const warningTitle = isActive ? 'Active task' : 'This cannot be undone';
      const warningText  = isActive
        ? 'This task is still running or queued. It will be cancelled first, then deleted.'
        : 'The task and its logs will be permanently removed.';

      const bodyHtml = `
        <div class="eo-empty-state danger" role="status">
          <div class="eo-empty-state-icon">
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
              <line x1="12" y1="9" x2="12" y2="13"></line>
              <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
          </div>
          <div class="eo-empty-state-body">
            <p class="eo-empty-state-title">${escHtml(warningTitle)}</p>
            <p class="eo-empty-state-text">${escHtml(warningText)}</p>
          </div>
        </div>
        ${window.eoBadgeRow ? window.eoBadgeRow([
          { label: `Status: ${taskStatus}`, variant: isActive ? 'warning' : 'info' }
        ]) : ''}
      `;

      let decided = false;
      const finish = (val, modal) => {
        if (decided) return;
        decided = true;
        if (modal) modal.close();
        resolve(val);
      };

      window.openEoModal({
        id: 'delete-task-modal',
        size: 'sm',
        eyebrow: 'Delete Task',
        title: taskLabel,
        desc: 'Confirm to delete this task.',
        body: bodyHtml,
        actions: [
          { label: 'Delete Task', variant: 'danger',    onClick: (m) => finish(true,  m) },
          { label: 'Cancel',      variant: 'secondary', onClick: (m) => finish(false, m) }
        ],
        onClose: () => finish(false, null)
      });
    });
  }

  // Resolves once the task reaches completed/failed/cancelled, or after the
  // timeout. Used by deleteTask so a cancel request has time to take effect
  // before we issue the DELETE that would otherwise 409.
  _waitForTaskTerminal(taskId, timeoutMs = 15_000) {
    return new Promise((resolve) => {
      // Already terminal in the bus cache? No need to wait.
      const cached = (window.taskEventBus && window.taskEventBus.getTask) ? window.taskEventBus.getTask(taskId) : null;
      if (cached && (cached.status === 'completed' || cached.status === 'failed' || cached.status === 'cancelled')) {
        return resolve(cached);
      }

      let timer;
      const cleanup = () => {
        window.removeEventListener('taskCompleted', onComplete);
        window.removeEventListener('taskUpdated', onUpdate);
        if (timer) clearTimeout(timer);
      };
      const isTerminal = (s) => s === 'completed' || s === 'failed' || s === 'cancelled';
      const onComplete = (e) => {
        const t = e.detail && (e.detail.task || (e.detail.taskId === taskId ? { id: taskId, status: e.detail.status } : null));
        const id = (t && t.id) || (e.detail && e.detail.taskId);
        if (id !== taskId) return;
        cleanup();
        resolve(t);
      };
      const onUpdate = (e) => {
        const t = e.detail && e.detail.task;
        if (!t || t.id !== taskId) return;
        if (isTerminal(t.status)) { cleanup(); resolve(t); }
      };
      window.addEventListener('taskCompleted', onComplete);
      window.addEventListener('taskUpdated', onUpdate);
      timer = setTimeout(() => { cleanup(); resolve(null); }, timeoutMs);
    });
  }

  async clearAllTasks() {
    // Use the confirmation dialog system if available, otherwise fallback to confirm
    return new Promise((resolve) => {
      if (window.showConfirmation) {
        window.showConfirmation(
          'Clear All Tasks',
          'Are you sure you want to clear all tasks? This will delete all task history and cannot be undone.',
          () => {
            this.performClearAll();
            resolve(true);
          },
          'Yes, Clear All',
          'Cancel',
          'clear',
          false
        );
      } else {
        // Fallback to native confirm
        const confirmed = confirm('Are you sure you want to clear all tasks? This will delete all task history.');
        if (confirmed) {
          this.performClearAll();
        }
        resolve(confirmed);
      }
    });
  }

  async performClearAll() {
    // Show progress notification with the trash type icon in the left slot
    // (this is conceptually a delete-all action, so 🗑️ matches the row icon).
    const customIcon = '<span style="font-size:18px;line-height:1;">🗑️</span>';
    const progressNotification = window.notificationSystem.show(
      'Clearing all tasks...',
      'info',
      null,
      null,
      null,
      customIcon
    );

    try {
      // Delete all tasks using the task manager
      const deletePromises = this.tasks.map(task => 
        this.taskManager.deleteTask(task.id)
      );
      
      await Promise.all(deletePromises);
      
      // Clear local array and re-render
      this.tasks = [];
      this.renderTasks();
      this.updateStats();
      this.updateSidebarCounts();
      this.generateAppCategories();
      
      // Remove progress notification and show success
      if (progressNotification && progressNotification.remove) {
        progressNotification.remove();
      }
      
      if (window.notificationSystem) {
        window.notificationSystem.show(
          'All tasks cleared successfully',
          'success',
          null,
          null,
          null,
          customIcon
        );
      }
    } catch (error) {
      console.error('Error clearing tasks:', error);
      
      // Remove progress notification and show error
      if (progressNotification && progressNotification.remove) {
        progressNotification.remove();
      }
      
      if (window.notificationSystem) {
        window.notificationSystem.show(
          `Failed to clear tasks: ${error.message}`,
          'error',
          null,
          null,
          null,
          customIcon
        );
      }
    }
  }

  getTimeAgo(dateString) {
    const date = new Date(dateString);
    const now = new Date();
    const diffMs = now - date;
    const diffMins = Math.floor(diffMs / 60000);
    
    if (diffMins < 1) return 'just now';
    if (diffMins < 60) return `${diffMins}m ago`;
    
    const diffHours = Math.floor(diffMins / 60);
    if (diffHours < 24) return `${diffHours}h ago`;
    
    const diffDays = Math.floor(diffHours / 24);
    return `${diffDays}d ago`;
  }

  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  showError(message) {
    const container = document.getElementById('tasks-list');
    if (container) {
      container.innerHTML = `
        <div class="terminal-error">
          <div class="error-text">$ ${message}</div>
          <div class="error-cursor">_</div>
        </div>
      `;
    }
  }

  destroy() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
    }
  }
}

// Export for use in other modules
window.TasksManager = TasksManager;
