// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base.
Object.assign(TasksManager.prototype, {
  formatCommandForUser(task) {
    if (!task.command) return 'Unknown Task';

    // Declarative pattern table — order matters (first match wins, so put
    // specific patterns ahead of catch-alls). `title` is either a literal
    // string or a function taking the regex match and returning the title;
    // the latter is used for per-app patterns that extract the app slug.
    //
    // Adding a new task: just append one row here. Add the matching command
    // shape in the WebUI submission site and you're done — no new branch
    // needed in this function.
    const displayName = (slug) => window.getAppDisplayName ? window.getAppDisplayName(slug) : (slug.charAt(0).toUpperCase() + slug.slice(1));

    const PATTERNS = [
      // -- System / setup ----------------------------------------------------
      { match: /^libreportal setup config\b/,     title: 'LibrePortal - Apply Configuration' },
      { match: /^libreportal setup finalize\b/,   title: 'LibrePortal - Finalize Setup' },
      { match: /^libreportal setup apply\b/,      title: 'LibrePortal - Setup Wizard' },
      { match: /^libreportal config update\b/,    title: 'LibrePortal - Apply Configuration' },

      // -- Self-update -------------------------------------------------------
      { match: /^libreportal update (apply|now)\b/, title: 'LibrePortal - Update' },
      { match: /^libreportal update check\b/,       title: 'LibrePortal - Check for Updates' },

      // -- App updater (per-app updates + CVE scan; snapshots before applying)
      { match: /^libreportal updater check\b/,        title: 'Apps - Check for Updates' },
      { match: /^libreportal updater apply-all\b/,    title: 'Apps - Update All' },
      { match: /^libreportal updater apply (\S+)/,    title: (m) => `${displayName(m[1])} - Update` },
      { match: /^libreportal updater rollback (\S+)/, title: (m) => `${displayName(m[1])} - Roll Back` },
      { match: /^libreportal artifact apply (\S+)/,   title: (m) => `Hotfix ${m[1]} - Apply` },
      { match: /^libreportal artifact revert (\S+)/,  title: (m) => `Hotfix ${m[1]} - Revert` },

      // -- Peers -------------------------------------------------------------
      { match: /^libreportal peer add\b/,    title: 'LibrePortal - Add Peer' },
      { match: /^libreportal peer remove\b/, title: 'LibrePortal - Remove Peer' },
      { match: /^libreportal peer pair\b/,   title: 'LibrePortal - Pair with Peer' },

      // -- Regen -------------------------------------------------------------
      { match: /^libreportal regen\b/, title: 'LibrePortal - Regenerate WebUI Data' },

      // -- System maintenance ------------------------------------------------
      { match: /^libreportal system reclaim\b/,  title: 'LibrePortal - Reclaim Space' },
      { match: /^libreportal system image rm\b/, title: 'LibrePortal - Remove Images' },
      { match: /^libreportal verify\b/,          title: 'LibrePortal - Verify System' },

      // -- Backup: per-app (these capture the app slug) ----------------------
      { match: /^libreportal backup app create (\w+)/,     title: (m) => `${displayName(m[1])} - Create Backup` },
      { match: /^libreportal backup app schedule (\w+)/,   title: (m) => `${displayName(m[1])} - Scheduled Backup` },
      { match: /^libreportal backup app list (\w+)/,       title: (m) => `${displayName(m[1])} - List Backups` },
      { match: /^libreportal backup app delete_all (\w+)/, title: (m) => `${displayName(m[1])} - Delete All Backups` },
      { match: /^libreportal backup app delete (\w+)/,     title: (m) => `${displayName(m[1])} - Delete Backup` },

      // -- Backup: system / locations ----------------------------------------
      { match: /^libreportal backup all\b/,                title: 'LibrePortal - Backup All Apps' },
      { match: /^libreportal backup verify\b/,             title: 'LibrePortal - Verify Backups' },
      { match: /^libreportal backup system\b/,             title: 'LibrePortal - Backup Configs' },
      { match: /^libreportal backup location add\b/,       title: 'LibrePortal - Add Backup Location' },
      { match: /^libreportal backup location remove\b/,    title: 'LibrePortal - Remove Backup Location' },
      { match: /^libreportal backup location init\b/,      title: 'LibrePortal - Initialise Backup Locations' },
      { match: /^libreportal backup location check\b/,     title: 'LibrePortal - Check Backup Locations' },
      { match: /^libreportal backup location list\b/,      title: 'LibrePortal - List Backup Locations' },
      { match: /^libreportal backup location stats\b/,     title: 'LibrePortal - Backup Location Stats' },

      // -- Restore / migrate -------------------------------------------------
      { match: /^libreportal restore app start (\w+)/,         title: (m) => `${displayName(m[1])} - Restore Backup` },
      { match: /^libreportal restore app list (\w+)/,          title: (m) => `${displayName(m[1])} - List Backups` },
      { match: /^libreportal restore migrate app (\w+)/,       title: (m) => `${displayName(m[1])} - Migrate from Host` },
      { match: /^libreportal restore migrate system\b/,        title: 'LibrePortal - Migrate System' },
      { match: /^libreportal restore migrate discover\b/,      title: 'LibrePortal - Discover Backups' },
      { match: /^libreportal restore first-run\b/,             title: 'LibrePortal - First-Run Restore' },
    ];

    for (const p of PATTERNS) {
      const m = task.command.match(p.match);
      if (m) return typeof p.title === 'function' ? p.title(m) : p.title;
    }

    // `libreportal app tool <app> <tool_id> ['<args>']` — needs the tools
    // catalog to resolve the friendly label, so it lives outside the table.
    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}`;
    }

    // Generic `libreportal app <action> <app>` — capture the app token only;
    // anything after (e.g. config overrides `CFG_FOO=bar|…`) is for the CLI.
    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}`;
    }

    if (task.command.includes('docker-compose')) return 'Docker Compose Operation';
    if (task.command.includes('docker'))         return 'Docker Operation';

    return task.command.length > 50 ? task.command.substring(0, 47) + '...' : task.command;
  },
  // User-facing label for a task action. Explicit map first (so
  // 'config_update' reads as 'Update Config', not 'Config Update');
  // anything not listed falls back to snake/kebab → Title Case so a
  // future task type still renders cleanly instead of leaking the
  // literal id like "Config_update".
  formatActionTitle(action) {
    if (!action) return 'Task';
    const map = {
      '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',
      'config_update': 'Update Config', 'update_config': 'Update Config',
      'system_update': 'Update System', 'system_reclaim': 'Reclaim Space',
      'system_image_rm': 'Remove Images', 'verify': 'Verify System',
      'updater_check': 'Check for Updates', 'updater_apply': 'Update',
      'updater_apply_all': 'Update All', 'updater_rollback': 'Roll Back',
      'artifact_apply': 'Apply Hotfix', 'artifact_revert': 'Revert Hotfix',
      'setup-config': 'Apply Configuration',
      'setup-finalize': 'Finalize Setup'
    };
    if (map[action]) return map[action];
    return action.split(/[_-]/).map(w => w ? w.charAt(0).toUpperCase() + w.slice(1) : '').join(' ');
  },
  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);
  },
  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`;
  },
  extractTimestamp(logEntry) {
    const match = logEntry.match(/\[([^\]]+)\]/);
    return match ? match[1] : '';
  },
  extractLogMessage(logEntry) {
    const match = logEntry.match(/\] (.+)$/);
    return match ? match[1] : logEntry;
  },
  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  },
});
