  // Update selection state + toolbar buttons
  function updateFMSelection() {
    const all     = document.querySelectorAll('.fm-row-chk');
    const checked = document.querySelectorAll('.fm-row-chk:checked').length;
    const chkAll  = document.getElementById('fm-chk-all');
    if (chkAll) {
      chkAll.checked       = all.length > 0 && checked === all.length;
      chkAll.indeterminate = checked > 0 && checked < all.length;
    }
    const delBtn = document.getElementById('fm-btn-delete-sel');
    delBtn.style.display = checked > 0 ? 'inline-flex' : 'none';
    if (checked > 0) delBtn.textContent = `Delete (${checked})`;
    const renBtn = document.getElementById('fm-btn-rename-sel');
    renBtn.style.display = checked === 1 ? 'inline-flex' : 'none';
    // Update status bar selection count
    const selEl = document.getElementById('fm-status-sel');
    const selN  = document.getElementById('fm-status-sel-n');
    if (selEl) { selEl.style.display = checked > 0 ? 'inline' : 'none'; if (selN) selN.textContent = checked; }
  }/* ════════════════════════════════════════════════════════════════
   DeployPro — Frontend SPA
   ════════════════════════════════════════════════════════════════ */
'use strict';

const App = (() => {

  // ─── State ─────────────────────────────────────────────────────
  let ws = null;
  let wsRetryTimer = null;
  let deployChart = null;
  let managedSites = [];
  let allVersions = [];
  let currentPage = 'dashboard';
  let currentUser = null;  // Set on init from localStorage

  // File manager state
  let fm = {
    siteId: null,
    sitePath: '',
    currentPath: '/',
    editingFile: null,
  };

  // Rollback state
  let pendingRollback = null;

  // ─── API helpers ───────────────────────────────────────────────
  async function api(method, url, body) {
    const token = localStorage.getItem('dp_token');
    const opts = {
      method,
      headers: {},
    };
    if (token) opts.headers['Authorization'] = 'Bearer ' + token;
    if (body instanceof FormData) {
      opts.body = body;
    } else if (body) {
      opts.headers['Content-Type'] = 'application/json';
      opts.body = JSON.stringify(body);
    }
    const r = await fetch('/api' + url, opts);
    if (r.status === 401) {
      localStorage.removeItem('dp_token');
      localStorage.removeItem('dp_user');
      window.location.href = '/login';
      throw new Error('Session expired — please sign in again');
    }
    const json = await r.json();
    if (!r.ok) throw new Error(json.error || 'Request failed');
    return json;
  }

  const GET  = url        => api('GET', url);
  const POST = (url, b)   => api('POST', url, b);
  const PUT  = (url, b)   => api('PUT', url, b);
  const DEL  = (url, b)   => api('DELETE', url, b);

  // ─── Formatted helpers ─────────────────────────────────────────
  function fmtDate(iso) {
    if (!iso) return '—';
    const d = new Date(iso);
    return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
  }

  function fmtSize(bytes) {
    if (bytes === null || bytes === undefined) return '—';
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  }

  function statusBadge(status) {
    const map = {
      success:     '<span class="badge badge-success"><i class="fa-solid fa-check"></i> Success</span>',
      failed:      '<span class="badge badge-failed"><i class="fa-solid fa-xmark"></i> Failed</span>',
      'in-progress': '<span class="badge badge-inprogress">In Progress</span>',
    };
    return map[status] || `<span class="badge">${status}</span>`;
  }

  function stateBadge(state) {
    const s = (state || '').toLowerCase();
    if (s === 'started' || s === 'running')  return '<span class="badge badge-running"><i class="fa-solid fa-circle-dot"></i> Running</span>';
    if (s === 'stopped')                     return '<span class="badge badge-stopped"><i class="fa-solid fa-stop"></i> Stopped</span>';
    return `<span class="badge badge-info">${state}</span>`;
  }

  function levelBadge(level) {
    const map = {
      success: 'badge-success',
      error:   'badge-failed',
      warning: 'badge-inprogress',
      info:    'badge-info',
    };
    return `<span class="badge ${map[level] || 'badge-info'}">${level}</span>`;
  }

  // ─── Toast ────────────────────────────────────────────────────
  function toast(type, title, msg, duration = 4000) {
    const icons = { success: 'fa-circle-check', error: 'fa-circle-xmark', warning: 'fa-triangle-exclamation', info: 'fa-circle-info' };
    const div = document.createElement('div');
    div.className = `toast toast-${type}`;
    div.innerHTML = `
      <i class="toast-icon fa-solid ${icons[type] || 'fa-circle-info'}"></i>
      <div class="toast-body">
        <div class="toast-title">${esc(title)}</div>
        ${msg ? `<div class="toast-msg">${esc(msg)}</div>` : ''}
      </div>`;
    const container = document.getElementById('toast-container');
    container.appendChild(div);
    setTimeout(() => {
      div.style.opacity = '0';
      div.style.transform = 'translateX(40px)';
      div.style.transition = 'all 0.3s';
      setTimeout(() => div.remove(), 300);
    }, duration);
  }

  function esc(str) {
    if (!str) return '';
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  // ─── Confirm dialog ───────────────────────────────────────────
  function confirm(title, body, okLabel = 'Confirm', okClass = 'btn-danger') {
    return new Promise(resolve => {
      const overlay = document.getElementById('modal-confirm');
      document.getElementById('modal-confirm-title').textContent = title;
      document.getElementById('modal-confirm-body').innerHTML = body;
      const okBtn = document.getElementById('modal-confirm-ok');
      okBtn.textContent = okLabel;
      okBtn.className = `btn ${okClass}`;
      overlay.style.display = 'flex';

      const cleanup = (val) => {
        overlay.style.display = 'none';
        okBtn.onclick = null;
        document.getElementById('modal-confirm-cancel').onclick = null;
        resolve(val);
      };
      okBtn.onclick = () => cleanup(true);
      document.getElementById('modal-confirm-cancel').onclick = () => cleanup(false);
    });
  }

  function closeModal(id) {
    document.getElementById(id).style.display = 'none';
  }

  // ─── Navigation ───────────────────────────────────────────────
  function navigate(page) {
    document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
    document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));

    const targetPage = document.getElementById('page-' + page);
    if (targetPage) {
      targetPage.classList.add('active');
      targetPage.style.display = '';
      currentPage = page;
    }

    const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
    if (navItem) navItem.classList.add('active');

    const titles = {
      dashboard: 'Dashboard', deploy: 'New Deployment',
      versions: 'Versions & Rollback', iis: 'IIS Manager',
      files: 'File Manager', sites: 'Managed Sites',
      activity: 'Activity Log', settings: 'Settings',
      users: 'User Management',
    };
    document.getElementById('topbar-title').textContent = titles[page] || page;

    // Lazy-load pages
    if (page === 'dashboard') loadDashboard();
    if (page === 'versions')  loadVersions();
    if (page === 'iis')       { loadIISSites(); loadIISPools(); }
    if (page === 'sites')     loadManagedSites();
    if (page === 'activity')  loadActivity();
    if (page === 'settings')  loadSettings();
    if (page === 'deploy')    populateSiteDropdowns();
    if (page === 'files')     populateSiteDropdowns();
    if (page === 'users')     loadUsers();
  }

  // ─── WebSocket ────────────────────────────────────────────────
  function connectWS() {
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    ws = new WebSocket(`${proto}://${location.host}/ws`);

    ws.onopen = () => {
      clearTimeout(wsRetryTimer);
      document.getElementById('ws-dot') && (document.getElementById('ws-indicator').className = 'ws-dot connected');
      document.getElementById('ws-label').textContent = 'Live';
    };

    ws.onmessage = (e) => {
      try {
        const msg = JSON.parse(e.data);
        handleWsMessage(msg);
      } catch { /* ignore */ }
    };

    ws.onclose = () => {
      const ind = document.getElementById('ws-indicator');
      if (ind) ind.className = 'ws-dot disconnected';
      document.getElementById('ws-label').textContent = 'Reconnecting…';
      wsRetryTimer = setTimeout(connectWS, 3000);
    };
    ws.onerror = () => ws.close();
  }

  function handleWsMessage(msg) {
    if (msg.type === 'log') {
      appendDeployLog(msg.message, msg.level);
      appendLiveFeed(msg.message, msg.level);
    }
    if (msg.type === 'event') {
      if (msg.event === 'deployComplete' || msg.event === 'rollbackComplete') {
        const ok = msg.data.status === 'success';
        toast(ok ? 'success' : 'error',
          msg.event === 'rollbackComplete' ? 'Rollback' : 'Deployment',
          ok ? 'Completed successfully' : 'Encountered errors');
        document.getElementById('btn-deploy').disabled = false;
        document.getElementById('btn-deploy').innerHTML = '<i class="fa-solid fa-rocket"></i> Deploy Now';
        if (currentPage === 'versions') loadVersions();
        if (currentPage === 'dashboard') loadDashboard();
      }
      if (msg.event === 'activity') {
        if (currentPage === 'activity') loadActivity();
      }
    }
  }

  // ─── Deploy log ───────────────────────────────────────────────
  function appendDeployLog(msg, level = 'info') {
    const box = document.getElementById('deploy-log');
    const idle = box.querySelector('.console-idle');
    if (idle) idle.remove();

    const ts = new Date().toLocaleTimeString();
    const div = document.createElement('div');
    div.className = `log-${level}`;
    div.innerHTML = `<span class="log-ts">[${esc(ts)}]</span>${esc(msg)}`;
    box.appendChild(div);
    box.scrollTop = box.scrollHeight;
  }

  function appendLiveFeed(msg, level = 'info') {
    const feed = document.getElementById('live-feed');
    if (!feed) return;
    const ts = new Date().toLocaleTimeString();
    const div = document.createElement('div');
    div.className = `feed-item feed-${level}`;
    div.innerHTML = `<span class="feed-ts">${esc(ts)}</span><span class="feed-msg">${esc(msg)}</span>`;
    feed.prepend(div);
    // Trim feed to 50 items
    while (feed.children.length > 50) feed.lastChild.remove();
  }

  function clearLiveFeed() {
    document.getElementById('live-feed').innerHTML = '';
  }

  // ─── Dashboard ───────────────────────────────────────────────
  async function loadDashboard() {
    try {
      const data = await GET('/dashboard');
      const s = data.stats;
      document.getElementById('stat-sites').textContent    = s.totalSites;
      document.getElementById('stat-total').textContent    = s.totalDeployments;
      document.getElementById('stat-success').textContent  = s.successfulDeployments;
      document.getElementById('stat-failed').textContent   = s.failedDeployments;

      // Chart
      const labels = data.chartData.map(d => d.day.slice(5));
      const counts = data.chartData.map(d => d.count);
      const ctx = document.getElementById('chart-deploys').getContext('2d');
      if (deployChart) deployChart.destroy();
      deployChart = new Chart(ctx, {
        type: 'bar',
        data: {
          labels,
          datasets: [{
            label: 'Deployments',
            data: counts,
            backgroundColor: 'rgba(79,70,229,0.5)',
            borderColor: '#4f46e5',
            borderWidth: 1,
            borderRadius: 4,
          }],
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { display: false } },
          scales: {
            x: { ticks: { color: '#475569' }, grid: { color: '#1e2d4a' } },
            y: { ticks: { color: '#475569', stepSize: 1 }, grid: { color: '#1e2d4a' }, beginAtZero: true },
          },
        },
      });

      // Recent deployments
      const tbody = document.getElementById('tbody-recent');
      tbody.innerHTML = data.recent.length
        ? data.recent.map(d => `
            <tr>
              <td><strong>${esc(d.siteName)}</strong></td>
              <td><code>${esc(d.version)}</code></td>
              <td>${statusBadge(d.status)}</td>
              <td>${esc(fmtDate(d.deployedAt))}</td>
              <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(d.notes)}</td>
            </tr>`).join('')
        : '<tr><td colspan="5" class="empty-row">No deployments yet.</td></tr>';

    } catch (err) {
      toast('error', 'Dashboard', err.message);
    }
  }

  // ─── Populate site dropdowns ──────────────────────────────────
  async function populateSiteDropdowns() {
    try {
      managedSites = await GET('/sites/managed');
    } catch { managedSites = []; }

    const selectors = ['dep-site', 'ver-site-filter', 'fm-site-select'];
    selectors.forEach(id => {
      const el = document.getElementById(id);
      if (!el) return;
      const prev = el.value;
      const isFilter = (id === 'ver-site-filter');
      el.innerHTML = (isFilter ? '<option value="">All Sites</option>' : '<option value="">— Select a managed site —</option>');
      managedSites.forEach(s => {
        const opt = document.createElement('option');
        opt.value = s.id;
        opt.textContent = s.name;
        if (opt.value === prev) opt.selected = true;
        el.appendChild(opt);
      });
    });

    // When on File Manager with no sites, show a clear redirect message
    if (managedSites.length === 0 && currentPage === 'files') {
      document.getElementById('fm-tree').innerHTML =
        `<div class="fm-placeholder" style="padding:20px;text-align:center;line-height:1.6">
          <i class="fa-solid fa-server" style="font-size:28px;color:var(--text-3);display:block;margin-bottom:10px"></i>
          <strong style="display:block;margin-bottom:6px">No sites configured</strong>
          <span style="font-size:12px;color:var(--text-3)">
            Go to <a href="#" onclick="App.navigate('sites');return false" style="color:var(--indigo)">
              Managed Sites
            </a> to register or import your IIS sites first.
          </span>
        </div>`;
      document.getElementById('fm-tbody').innerHTML = '<tr><td colspan="4" class="empty-row">—</td></tr>';
    }
  }

  // ─── Versions ─────────────────────────────────────────────────
  async function loadVersions() {
    await populateSiteDropdowns();
    try {
      allVersions = await GET('/versions');
      renderVersions();
    } catch (err) {
      toast('error', 'Versions', err.message);
    }
  }

  function renderVersions() {
    const filter = document.getElementById('ver-site-filter').value;
    const data = filter ? allVersions.filter(v => v.siteId === filter) : allVersions;
    const tbody = document.getElementById('tbody-versions');
    tbody.innerHTML = data.length ? data.map(d => `
      <tr>
        <td><strong>${esc(d.siteName)}</strong></td>
        <td><code>${esc(d.version)}</code></td>
        <td>${statusBadge(d.status)}</td>
        <td>${d.isRollback ? '<span class="badge badge-rollback"><i class="fa-solid fa-rotate-left"></i> Rollback</span>' : '<span class="badge badge-info">Deploy</span>'}</td>
        <td>${d.backupPath ? '<span class="badge badge-success" title="' + esc(d.backupPath) + '"><i class="fa-solid fa-box-archive"></i> Yes</span>' : '<span class="badge" style="color:var(--text-3)">—</span>'}</td>
        <td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(d.notes)}</td>
        <td style="white-space:nowrap">${esc(fmtDate(d.deployedAt))}</td>
        <td class="actions-cell">
          ${d.backupPath && d.status === 'success' ? `
            <button class="btn btn-warning btn-xs" onclick="App.openRollback('${esc(d.id)}','${esc(d.siteName)}','${esc(d.version)}')">
              <i class="fa-solid fa-rotate-left"></i> Rollback
            </button>` : ''}
        </td>
      </tr>`).join('')
    : '<tr><td colspan="8" class="empty-row">No deployments recorded.</td></tr>';
  }

  function openRollback(deploymentId, siteName, version) {
    pendingRollback = deploymentId;
    document.getElementById('rollback-confirm-text').innerHTML =
      `Roll back <strong>${esc(siteName)}</strong> to version <strong>${esc(version)}</strong>?
       <br/><small style="color:var(--text-3)">The backup archive for this version will be restored.</small>`;
    document.getElementById('modal-rollback').style.display = 'flex';
  }

  async function executeRollback() {
    if (!pendingRollback) return;
    closeModal('modal-rollback');
    const stopPool = document.getElementById('rb-stop-pool').checked;
    const iisReset = document.getElementById('rb-iis-reset').checked;
    try {
      await POST('/rollback', { deploymentId: pendingRollback, stopPool, iisResetAfter: iisReset });
      toast('info', 'Rollback', 'Rollback started — check console for progress');
    } catch (err) {
      toast('error', 'Rollback failed', err.message);
    }
    pendingRollback = null;
  }

  // ─── IIS Manager ─────────────────────────────────────────────
  async function loadIISSites() {
    document.getElementById('tbody-iis-sites').innerHTML = '<tr><td colspan="4" class="empty-row">Loading…</td></tr>';
    try {
      const result = await GET('/iis/sites');
      const tbody = document.getElementById('tbody-iis-sites');
      if (!result.ok && result.data.length === 0) {
        tbody.innerHTML = `<tr><td colspan="4" class="empty-row" style="color:var(--yellow)"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(result.error || 'IIS not accessible — run server as Administrator')}</td></tr>`;
        return;
      }
      tbody.innerHTML = result.data.map(s => `
        <tr>
          <td><strong>${esc(s.Name)}</strong></td>
          <td>${stateBadge(s.State)}</td>
          <td style="font-family:var(--font-mono);font-size:12px;max-width:240px;overflow:hidden;text-overflow:ellipsis">${esc(s.PhysicalPath)}</td>
          <td class="actions-cell">
            <button class="btn btn-success btn-xs" onclick="App.iisControlSite('${esc(s.Name)}','start')"><i class="fa-solid fa-play"></i></button>
            <button class="btn btn-ghost btn-xs"   onclick="App.iisControlSite('${esc(s.Name)}','stop')"><i class="fa-solid fa-stop"></i></button>
          </td>
        </tr>`).join('') || '<tr><td colspan="4" class="empty-row">No IIS sites found.</td></tr>';
    } catch (err) {
      document.getElementById('tbody-iis-sites').innerHTML = `<tr><td colspan="4" class="empty-row" style="color:var(--red)">${esc(err.message)}</td></tr>`;
    }
  }

  async function loadIISPools() {
    document.getElementById('tbody-iis-pools').innerHTML = '<tr><td colspan="4" class="empty-row">Loading…</td></tr>';
    try {
      const result = await GET('/iis/pools');
      const tbody = document.getElementById('tbody-iis-pools');
      if (!result.ok && result.data.length === 0) {
        tbody.innerHTML = `<tr><td colspan="4" class="empty-row" style="color:var(--yellow)">IIS not accessible</td></tr>`;
        return;
      }
      tbody.innerHTML = result.data.map(p => `
        <tr>
          <td><strong>${esc(p.Name)}</strong></td>
          <td>${stateBadge(p.State)}</td>
          <td style="font-size:12px">${esc(p.ManagedRuntimeVersion) || 'No Managed Code'}</td>
          <td class="actions-cell">
            <button class="btn btn-success btn-xs" onclick="App.iisControlPool('${esc(p.Name)}','start')"><i class="fa-solid fa-play"></i></button>
            <button class="btn btn-ghost btn-xs"   onclick="App.iisControlPool('${esc(p.Name)}','stop')"><i class="fa-solid fa-stop"></i></button>
            <button class="btn btn-warning btn-xs" onclick="App.iisControlPool('${esc(p.Name)}','recycle')"><i class="fa-solid fa-rotate"></i> Recycle</button>
          </td>
        </tr>`).join('') || '<tr><td colspan="4" class="empty-row">No pools found.</td></tr>';
    } catch (err) {
      document.getElementById('tbody-iis-pools').innerHTML = `<tr><td colspan="4" class="empty-row" style="color:var(--red)">${esc(err.message)}</td></tr>`;
    }
  }

  async function iisControlSite(name, action) {
    try {
      await POST(`/iis/sites/${encodeURIComponent(name)}/${action}`);
      toast('success', 'IIS', `Site "${name}" ${action}ed`);
      setTimeout(loadIISSites, 600);
    } catch (err) {
      toast('error', 'IIS Error', err.message);
    }
  }

  async function iisControlPool(name, action) {
    try {
      await POST(`/iis/pools/${encodeURIComponent(name)}/${action}`);
      toast('success', 'App Pool', `Pool "${name}" ${action}d`);
      setTimeout(loadIISPools, 600);
    } catch (err) {
      toast('error', 'IIS Error', err.message);
    }
  }

  async function triggerIISReset() {
    const ok = await confirm(
      'IIS Reset',
      '<p>This will run <code>iisreset</code> which <strong>stops all IIS services</strong> briefly. All active requests will be interrupted.</p><p style="margin-top:8px">Are you sure?</p>',
      'Run iisreset', 'btn-danger'
    );
    if (!ok) return;
    toast('warning', 'IIS Reset', 'Reset in progress…', 8000);
    try {
      const r = await POST('/iis/reset');
      toast(r.success ? 'success' : 'warning', 'IIS Reset', r.success ? 'Completed successfully' : r.stderr);
      setTimeout(() => { loadIISSites(); loadIISPools(); }, 2000);
    } catch (err) {
      toast('error', 'IIS Reset failed', err.message);
    }
  }

  // ─── Managed Sites ────────────────────────────────────────────
  async function loadManagedSites() {
    const tbody = document.getElementById('tbody-managed-sites');
    tbody.innerHTML = '<tr><td colspan="5" class="empty-row">Loading…</td></tr>';
    try {
      managedSites = await GET('/sites/managed');
      tbody.innerHTML = managedSites.length
        ? managedSites.map(s => `
            <tr>
              <td><strong>${esc(s.name)}</strong>${s.description ? `<br/><small style="color:var(--text-3)">${esc(s.description)}</small>` : ''}</td>
              <td><code>${esc(s.iisName)}</code></td>
              <td style="font-family:var(--font-mono);font-size:12px">${esc(s.deployPath)}</td>
              <td>${s.appPool ? `<code>${esc(s.appPool)}</code>` : '<span style="color:var(--text-3)">—</span>'}</td>
              <td class="actions-cell">
                <button class="btn btn-ghost btn-xs" onclick="App.openEditSite('${esc(s.id)}')" title="Edit"><i class="fa-solid fa-pen"></i></button>
                <button class="btn btn-danger btn-xs" onclick="App.deleteSite('${esc(s.id)}','${esc(s.name)}')" title="Remove"><i class="fa-solid fa-trash"></i></button>
              </td>
            </tr>`).join('')
        : `<tr><td colspan="5" style="padding:40px 24px;text-align:center;border:0">
            <i class="fa-solid fa-server" style="font-size:36px;color:var(--text-3);display:block;margin-bottom:12px"></i>
            <div style="font-size:15px;font-weight:600;margin-bottom:6px">No sites registered yet</div>
            <div style="color:var(--text-3);margin-bottom:20px;font-size:13px">
              Register your IIS sites to start deploying. Add manually or auto-import from IIS.
            </div>
            <button class="btn btn-primary btn-sm" onclick="App.openAddSite()" style="margin-right:8px">
              <i class="fa-solid fa-plus"></i> Add Site Manually
            </button>
            <button class="btn btn-ghost btn-sm" onclick="App.importFromIIS()">
              <i class="fa-brands fa-windows"></i> Import from IIS
            </button>
          </td></tr>`;
    } catch (err) {
      toast('error', 'Sites', err.message);
      tbody.innerHTML = `<tr><td colspan="5" class="empty-row" style="color:var(--red)">${esc(err.message)}</td></tr>`;
    }
  }

  async function importFromIIS() {
    const btn = document.getElementById('btn-import-iis');
    if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Importing…'; }
    try {
      const r = await POST('/sites/import-from-iis');
      if (r.imported === 0) {
        toast('info', 'Import from IIS', 'All IIS sites are already registered (or IIS returned no sites).');
      } else {
        toast('success', 'Import from IIS', `${r.imported} site${r.imported > 1 ? 's' : ''} imported successfully`);
      }
      await loadManagedSites();
      await populateSiteDropdowns();
    } catch (err) {
      toast('error', 'Import failed', err.message);
    } finally {
      if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-brands fa-windows"></i> Import from IIS'; }
    }
  }

  function openAddSite() {
    document.getElementById('site-edit-id').value = '';
    document.getElementById('modal-site-title').textContent = 'Add Managed Site';
    ['site-name','site-iis-name','site-deploy-path','site-app-pool','site-description']
      .forEach(id => { document.getElementById(id).value = ''; });
    document.getElementById('modal-site').style.display = 'flex';
  }

  function openEditSite(id) {
    const s = managedSites.find(x => x.id === id);
    if (!s) return;
    document.getElementById('site-edit-id').value = id;
    document.getElementById('modal-site-title').textContent = 'Edit Site';
    document.getElementById('site-name').value        = s.name;
    document.getElementById('site-iis-name').value    = s.iisName;
    document.getElementById('site-deploy-path').value = s.deployPath;
    document.getElementById('site-app-pool').value    = s.appPool || '';
    document.getElementById('site-description').value = s.description || '';
    document.getElementById('modal-site').style.display = 'flex';
  }

  function closeSiteModal() { closeModal('modal-site'); }

  async function saveSite() {
    const id = document.getElementById('site-edit-id').value;
    const body = {
      name:        document.getElementById('site-name').value.trim(),
      iisName:     document.getElementById('site-iis-name').value.trim(),
      deployPath:  document.getElementById('site-deploy-path').value.trim(),
      appPool:     document.getElementById('site-app-pool').value.trim(),
      description: document.getElementById('site-description').value.trim(),
    };
    if (!body.name || !body.deployPath) {
      toast('warning', 'Validation', 'Name and Deploy Path are required'); return;
    }
    try {
      if (id) {
        await PUT(`/sites/managed/${id}`, body);
        toast('success', 'Site Updated', body.name);
      } else {
        await POST('/sites/managed', body);
        toast('success', 'Site Added', body.name);
      }
      closeSiteModal();
      loadManagedSites();
      populateSiteDropdowns();
    } catch (err) {
      toast('error', 'Save failed', err.message);
    }
  }

  async function deleteSite(id, name) {
    const ok = await confirm('Delete Site', `Remove <strong>${esc(name)}</strong> from DeployPro?<br><small style="color:var(--text-3)">This only removes the site configuration — no files are deleted.</small>`, 'Delete', 'btn-danger');
    if (!ok) return;
    try {
      await DEL('/sites/managed/' + id);
      toast('success', 'Site removed', name);
      loadManagedSites();
      populateSiteDropdowns();
    } catch (err) {
      toast('error', 'Error', err.message);
    }
  }

  // ─── Deploy form ──────────────────────────────────────────────
  let selectedFile        = null;   // ZIP mode
  let selectedFiles       = [];     // Files / Folder mode
  let deploySource        = 'zip';  // 'zip' | 'files' | 'folder'
  let retainTreeChecked   = new Set(); // paths checked in the visual picker
  let retainTreeSiteId    = null;      // siteId whose tree is currently loaded

  function initDeployForm() {
    // Source tab switching
    document.querySelectorAll('.dep-src-tab').forEach(tab => {
      tab.addEventListener('click', () => {
        deploySource = tab.dataset.src;
        document.querySelectorAll('.dep-src-tab').forEach(t => t.classList.remove('active'));
        tab.classList.add('active');
        document.getElementById('dep-src-zip').style.display    = deploySource === 'zip'    ? '' : 'none';
        document.getElementById('dep-src-files').style.display  = deploySource === 'files'  ? '' : 'none';
        document.getElementById('dep-src-folder').style.display = deploySource === 'folder' ? '' : 'none';
        // Reset selections when switching tabs
        selectedFile  = null;
        selectedFiles = [];
      });
    });

    // ZIP drop-zone
    const dropZone  = document.getElementById('drop-zone');
    const fileInput = document.getElementById('dep-file');
    dropZone.addEventListener('click', () => fileInput.click());
    fileInput.addEventListener('change', () => {
      if (fileInput.files[0]) selectZip(fileInput.files[0]);
    });
    dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragging'); });
    dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragging'));
    dropZone.addEventListener('drop', e => {
      e.preventDefault(); dropZone.classList.remove('dragging');
      const f = e.dataTransfer.files[0];
      if (f && f.name.toLowerCase().endsWith('.zip')) selectZip(f);
      else toast('warning', 'Deploy', 'Please drop a .zip file');
    });

    // Multiple files drop-zone
    const dropZoneFiles  = document.getElementById('drop-zone-files');
    const filesInput     = document.getElementById('dep-files-input');
    dropZoneFiles.addEventListener('click', () => filesInput.click());
    filesInput.addEventListener('change', () => {
      if (filesInput.files.length) selectMultiFiles(Array.from(filesInput.files), false);
    });
    dropZoneFiles.addEventListener('dragover', e => { e.preventDefault(); dropZoneFiles.classList.add('dragging'); });
    dropZoneFiles.addEventListener('dragleave', () => dropZoneFiles.classList.remove('dragging'));
    dropZoneFiles.addEventListener('drop', e => {
      e.preventDefault(); dropZoneFiles.classList.remove('dragging');
      const files = Array.from(e.dataTransfer.files).filter(f => !f.name.startsWith('.'));
      if (files.length) selectMultiFiles(files, false);
    });

    // Folder drop-zone
    const dropZoneFolder = document.getElementById('drop-zone-folder');
    const folderInput    = document.getElementById('dep-folder-input');
    dropZoneFolder.addEventListener('click', () => folderInput.click());
    folderInput.addEventListener('change', () => {
      if (folderInput.files.length) selectMultiFiles(Array.from(folderInput.files), true);
    });
    dropZoneFolder.addEventListener('dragover', e => { e.preventDefault(); dropZoneFolder.classList.add('dragging'); });
    dropZoneFolder.addEventListener('dragleave', () => dropZoneFolder.classList.remove('dragging'));
    dropZoneFolder.addEventListener('drop', e => {
      e.preventDefault(); dropZoneFolder.classList.remove('dragging');
      const files = Array.from(e.dataTransfer.files);
      if (files.length) selectMultiFiles(files, true);
    });

    document.getElementById('deploy-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      await runDeploy();
    });

    // Site selection → load file tree + saved config
    document.getElementById('dep-site').addEventListener('change', async function () {
      const siteId = this.value;
      // Reset tree state and textarea before loading new site
      retainTreeChecked = new Set();
      document.getElementById('dep-retain').value = '';
      const saveBtn = document.getElementById('btn-save-config');
      saveBtn.style.display = siteId ? '' : 'none';
      await loadRetainTree(siteId);
      if (siteId) await loadDeployConfig(siteId);
    });

    // Save Config button
    document.getElementById('btn-save-config').addEventListener('click', () => {
      const siteId = document.getElementById('dep-site').value;
      saveDeployConfig(siteId);
    });
  }

  function selectZip(file) {
    selectedFile = file;
    const sel = document.getElementById('drop-selected');
    sel.textContent = `✓ ${file.name} (${fmtSize(file.size)})`;
    sel.classList.remove('hidden');
  }

  function selectMultiFiles(files, isFolder) {
    selectedFiles = files;
    const selId = isFolder ? 'dep-folder-selected' : 'dep-files-selected';
    const sel   = document.getElementById(selId);
    const totalSize = files.reduce((s, f) => s + f.size, 0);
    if (isFolder) {
      const folderName = (files[0].webkitRelativePath || files[0].name).split('/')[0];
      sel.textContent = `✓ Folder: ${esc(folderName)}  ·  ${files.length} files  (${fmtSize(totalSize)})`;
    } else {
      sel.textContent = `✓ ${files.length} file${files.length > 1 ? 's' : ''} selected  (${fmtSize(totalSize)})`;
    }
    sel.classList.remove('hidden');
  }

  function getRetainPaths() {
    // Merge tree checked paths + manual textarea
    const fromTree     = [...retainTreeChecked];
    const fromTextarea = document.getElementById('dep-retain').value
      .split('\n').map(l => l.trim().replace(/\\/g, '/')).filter(Boolean);
    return [...new Set([...fromTree, ...fromTextarea])];
  }

  // ─── Retain Tree ─────────────────────────────────────────────

  function rtreeFileIcon(name) {
    const ext = (name.split('.').pop() || '').toLowerCase();
    const map = {
      config:'fa-solid fa-gear',         cfg:'fa-solid fa-gear',
      json:'fa-solid fa-code',           xml:'fa-solid fa-code',
      js:'fa-brands fa-js',              ts:'fa-solid fa-code',
      html:'fa-brands fa-html5',         htm:'fa-brands fa-html5',
      css:'fa-brands fa-css3-alt',       scss:'fa-brands fa-css3-alt',
      md:'fa-solid fa-file-lines',       txt:'fa-solid fa-file-lines',
      log:'fa-solid fa-file-lines',
      jpg:'fa-solid fa-file-image',      jpeg:'fa-solid fa-file-image',
      png:'fa-solid fa-file-image',      gif:'fa-solid fa-file-image',
      svg:'fa-solid fa-bezier-curve',    webp:'fa-solid fa-file-image',
      ico:'fa-solid fa-file-image',
      pdf:'fa-solid fa-file-pdf',
      zip:'fa-solid fa-file-zipper',     dll:'fa-solid fa-puzzle-piece',
      exe:'fa-solid fa-terminal',
    };
    return (map[ext] || 'fa-regular fa-file') + ' rtree-file-icon';
  }

  function renderRetainTreeItems(container, items, pathPrefix) {
    items.forEach(item => {
      // Normalise relative path: folders get trailing slash
      const relPath = item.path.replace(/^\//, '');
      const normPath = item.isDir ? relPath + '/' : relPath;

      const row = document.createElement('div');
      row.className = 'rtree-row';
      row.dataset.path = normPath;

      const chk = document.createElement('input');
      chk.type = 'checkbox';
      chk.className = 'rtree-chk';
      chk.checked = retainTreeChecked.has(normPath);
      if (chk.checked) row.classList.add('rtree-checked');

      chk.addEventListener('change', () => {
        if (chk.checked) retainTreeChecked.add(normPath);
        else             retainTreeChecked.delete(normPath);
        row.classList.toggle('rtree-checked', chk.checked);
      });

      if (item.isDir) {
        const expBtn = document.createElement('button');
        expBtn.type = 'button';
        expBtn.className = 'rtree-exp-btn';
        expBtn.innerHTML = '<i class="fa-solid fa-chevron-right rtree-chevron"></i>';

        const icon = document.createElement('i');
        icon.className = 'fa-solid fa-folder rtree-dir-icon';

        const label = document.createElement('span');
        label.className = 'rtree-name';
        label.textContent = item.name;

        const children = document.createElement('div');
        children.className = 'rtree-children';
        children.style.display = 'none';
        let childrenLoaded = false;

        const expand = async () => {
          const open = children.style.display !== 'none';
          if (open) {
            children.style.display = 'none';
            expBtn.querySelector('i').className = 'fa-solid fa-chevron-right rtree-chevron';
            icon.className = 'fa-solid fa-folder rtree-dir-icon';
          } else {
            children.style.display = '';
            expBtn.querySelector('i').className = 'fa-solid fa-chevron-down rtree-chevron';
            icon.className = 'fa-solid fa-folder-open rtree-dir-icon';
            if (!childrenLoaded) {
              childrenLoaded = true;
              children.innerHTML = '<div class="rtree-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';
              try {
                const data = await GET(`/files/${retainTreeSiteId}/browse?path=${encodeURIComponent(item.path)}`);
                children.innerHTML = '';
                if (data.items.length) renderRetainTreeItems(children, data.items, normPath);
                else children.innerHTML = '<div class="rtree-empty">Empty folder</div>';
              } catch (err) {
                children.innerHTML = `<div class="rtree-error"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;
              }
            }
          }
        };

        expBtn.addEventListener('click', expand);
        icon.addEventListener('click', expand);
        label.addEventListener('dblclick', expand);

        row.append(chk, expBtn, icon, label);
        container.append(row, children);
      } else {
        const icon = document.createElement('i');
        icon.className = rtreeFileIcon(item.name);

        const label = document.createElement('span');
        label.className = 'rtree-name';
        label.textContent = item.name;

        const size = document.createElement('span');
        size.className = 'rtree-size';
        size.textContent = fmtSize(item.size);

        row.append(chk, icon, label, size);
        container.append(row);
      }
    });
  }

  async function loadRetainTree(siteId) {
    retainTreeSiteId = siteId;
    const tree      = document.getElementById('retain-tree');
    const titleEl   = document.getElementById('retain-picker-title');
    const reloadBtn = document.getElementById('retain-reload-btn');

    if (!siteId) {
      titleEl.innerHTML = '<i class="fa-solid fa-circle-info"></i> Select a site above to browse its files';
      reloadBtn.style.display = 'none';
      tree.innerHTML = '';
      return;
    }

    titleEl.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Loading files…';
    reloadBtn.style.display = 'none';
    tree.innerHTML = '';

    try {
      const data = await GET(`/files/${siteId}/browse`);
      renderRetainTreeItems(tree, data.items, '');
      const siteName = (managedSites.find(s => s.id === siteId) || {}).name || siteId;
      titleEl.innerHTML = `<i class="fa-solid fa-folder-tree"></i> <strong>${esc(siteName)}</strong> — ${data.items.length} item${data.items.length !== 1 ? 's' : ''}`;
      reloadBtn.style.display = '';
      reloadBtn.onclick = () => { retainTreeChecked = new Set(); loadRetainTree(siteId); };
    } catch (err) {
      titleEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="color:var(--red)"></i> ${esc(err.message)}`;
      reloadBtn.style.display = '';
      reloadBtn.onclick = () => { retainTreeChecked = new Set(); loadRetainTree(siteId); };
    }
  }

  // ─── Deploy Config save / load ────────────────────────────────

  async function saveDeployConfig(siteId) {
    if (!siteId) return;
    try {
      await POST(`/deploy-config/${siteId}`, {
        retainFromTree:  [...retainTreeChecked],
        retainManual:    document.getElementById('dep-retain').value,
        optBackup:       document.getElementById('opt-backup').checked,
        optStopPool:     document.getElementById('opt-stop-pool').checked,
        optIisReset:     document.getElementById('opt-iis-reset').checked,
      });
      toast('success', 'Config', 'Deploy configuration saved');
    } catch (err) {
      toast('error', 'Config', err.message);
    }
  }

  async function loadDeployConfig(siteId) {
    if (!siteId) return;
    try {
      const cfg = await GET(`/deploy-config/${siteId}`);
      if (!cfg || !Object.keys(cfg).length) return;

      // Restore toggles
      if (cfg.optBackup   !== undefined) document.getElementById('opt-backup').checked    = cfg.optBackup;
      if (cfg.optStopPool !== undefined) document.getElementById('opt-stop-pool').checked  = cfg.optStopPool;
      if (cfg.optIisReset !== undefined) document.getElementById('opt-iis-reset').checked  = cfg.optIisReset;

      // Restore manual textarea
      if (cfg.retainManual !== undefined) document.getElementById('dep-retain').value = cfg.retainManual;

      // Restore tree selections (tree must already be rendered)
      if (Array.isArray(cfg.retainFromTree) && cfg.retainFromTree.length) {
        cfg.retainFromTree.forEach(p => {
          retainTreeChecked.add(p);
          const row = document.querySelector(`#retain-tree .rtree-row[data-path="${CSS.escape(p)}"]`);
          if (row) {
            const chk = row.querySelector('.rtree-chk');
            if (chk) { chk.checked = true; row.classList.add('rtree-checked'); }
          }
        });
      }
    } catch { /* silently ignore — no saved config */ }
  }

  async function runDeploy() {
    const siteId  = document.getElementById('dep-site').value;
    const version = document.getElementById('dep-version').value.trim();
    const notes   = document.getElementById('dep-notes').value.trim();

    if (!siteId)  { toast('warning', 'Deploy', 'Please select a site'); return; }
    if (!version) { toast('warning', 'Deploy', 'Please enter a version label'); return; }
    if (deploySource === 'zip' && !selectedFile) {
      toast('warning', 'Deploy', 'Please select a .zip package'); return;
    }
    if ((deploySource === 'files' || deploySource === 'folder') && !selectedFiles.length) {
      toast('warning', 'Deploy', 'Please select files / a folder'); return;
    }

    const btn = document.getElementById('btn-deploy');
    btn.disabled = true;
    btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Deploying…';

    document.getElementById('deploy-log').innerHTML = '';
    document.getElementById('deploy-progress-wrap').style.display = 'flex';
    document.getElementById('deploy-progress-fill').style.width = '10%';
    appendDeployLog('Uploading package…', 'info');

    const retainPaths = getRetainPaths();

    try {
      let deployPayload;

      if (deploySource === 'zip') {
        // Upload single zip
        const fd = new FormData();
        fd.append('package', selectedFile);
        const uploadRes = await api('POST', '/upload', fd);
        document.getElementById('deploy-progress-fill').style.width = '40%';
        appendDeployLog(`Package uploaded: ${uploadRes.filename}`, 'success');
        deployPayload = { packageFilename: uploadRes.filename };

      } else {
        // Upload multiple files / folder
        const isFolder = deploySource === 'folder';
        const fd = new FormData();
        const paths = [];
        selectedFiles.forEach(f => {
          fd.append('files', f);
          // For folder uploads webkitRelativePath gives "folder/sub/file.ext"
          // Strip the first path segment (selected folder name) for folder mode
          let rel = f.webkitRelativePath || f.name;
          if (isFolder) {
            const slash = rel.indexOf('/');
            rel = slash !== -1 ? rel.slice(slash + 1) : rel;
          }
          paths.push(rel);
        });
        fd.append('paths', JSON.stringify(paths));
        fd.append('isFolder', String(isFolder));

        const uploadRes = await api('POST', '/upload-multi', fd);
        document.getElementById('deploy-progress-fill').style.width = '40%';
        appendDeployLog(`${uploadRes.fileCount} file(s) uploaded`, 'success');
        deployPayload = { uploadId: uploadRes.uploadId };
      }

      // Trigger deploy
      await POST('/deploy', {
        siteId, version, notes,
        ...deployPayload,
        retainPaths,
        autoBackup:    document.getElementById('opt-backup').checked,
        stopPool:      document.getElementById('opt-stop-pool').checked,
        iisResetAfter: document.getElementById('opt-iis-reset').checked,
      });

      document.getElementById('deploy-progress-fill').style.width = '65%';
      appendDeployLog('Deployment pipeline started…', 'info');
      toast('info', 'Deployment', 'In progress — watch the console');

    } catch (err) {
      btn.disabled = false;
      btn.innerHTML = '<i class="fa-solid fa-rocket"></i> Deploy Now';
      document.getElementById('deploy-progress-fill').style.width = '0%';
      appendDeployLog(`Upload failed: ${err.message}`, 'error');
      toast('error', 'Deploy failed', err.message);
    }
  }

  // ─── File Manager ─────────────────────────────────────────────

  let fmItems  = [];
  let fmSort   = { col: 'name', dir: 'asc' };
  let fmCtxItem = null;

  function getFileIcon(name, isDir) {
    if (isDir) return '<i class="fa-solid fa-folder fmi-dir"></i>';
    const ext = (name.split('.').pop() || '').toLowerCase();
    const map = {
      js:'fa-brands fa-js fmi-js',       ts:'fa-solid fa-code fmi-ts',
      html:'fa-brands fa-html5 fmi-html', htm:'fa-brands fa-html5 fmi-html',
      css:'fa-brands fa-css3-alt fmi-css', scss:'fa-brands fa-css3-alt fmi-css',
      json:'fa-solid fa-code fmi-json',   xml:'fa-solid fa-code fmi-xml',
      md:'fa-solid fa-file-lines fmi-md', txt:'fa-solid fa-file-lines fmi-txt',
      log:'fa-solid fa-file-lines fmi-txt', csv:'fa-solid fa-file-lines fmi-txt',
      jpg:'fa-solid fa-file-image fmi-img', jpeg:'fa-solid fa-file-image fmi-img',
      png:'fa-solid fa-file-image fmi-img', gif:'fa-solid fa-file-image fmi-img',
      svg:'fa-solid fa-bezier-curve fmi-img', webp:'fa-solid fa-file-image fmi-img',
      ico:'fa-solid fa-file-image fmi-img',
      pdf:'fa-solid fa-file-pdf fmi-pdf',
      zip:'fa-solid fa-file-zipper fmi-zip', rar:'fa-solid fa-file-zipper fmi-zip',
      '7z':'fa-solid fa-file-zipper fmi-zip', gz:'fa-solid fa-file-zipper fmi-zip',
      sql:'fa-solid fa-database fmi-db',   db:'fa-solid fa-database fmi-db',
      cs:'fa-solid fa-code fmi-ts',        py:'fa-brands fa-python fmi-py',
      java:'fa-brands fa-java fmi-php',    php:'fa-brands fa-php fmi-php',
      dll:'fa-solid fa-microchip fmi-dll', exe:'fa-solid fa-terminal fmi-exe',
      ps1:'fa-brands fa-windows fmi-ps',   bat:'fa-solid fa-terminal fmi-bat',
      sh:'fa-solid fa-terminal fmi-sh',    config:'fa-solid fa-gear fmi-cfg',
      cfg:'fa-solid fa-gear fmi-cfg',      ini:'fa-solid fa-gear fmi-cfg',
      env:'fa-solid fa-gear fmi-cfg',      cshtml:'fa-brands fa-html5 fmi-html',
    };
    return `<i class="${map[ext] || 'fa-solid fa-file fmi-default'}"></i>`;
  }
  function initFileManager() {
    document.getElementById('fm-site-select').addEventListener('change', async (e) => {
      fm.siteId      = e.target.value;
      fm.currentPath = '/';
      fm.sitePath    = '';
      document.getElementById('fm-search').value = '';
      fmItems = [];
      if (!fm.siteId) {
        document.getElementById('fm-tree').innerHTML = '<div class="fm-placeholder">Select a site to browse files</div>';
        document.getElementById('fm-tbody').innerHTML = '<tr><td colspan="5" class="empty-row">—</td></tr>';
        document.getElementById('fm-status-text').textContent = 'No site selected';
        enableFMButtons(false);
        return;
      }
      const site = managedSites.find(s => s.id === fm.siteId);
      if (site) fm.sitePath = site.deployPath;
      enableFMButtons(true);
      await fmBrowse('/');
      await fmLoadTree('/');
    });

    document.getElementById('fm-btn-refresh').addEventListener('click', () => fmBrowse(fm.currentPath));
    document.getElementById('fm-btn-mkdir').addEventListener('click', fmMakeDir);
    document.getElementById('fm-btn-upload').addEventListener('click', () => document.getElementById('fm-upload-input').click());
    document.getElementById('fm-upload-input').addEventListener('change', async (e) => {
      const files = Array.from(e.target.files);
      e.target.value = '';
      if (files.length) await fmUploadFiles(files);
    });
    document.getElementById('fm-uq-close').addEventListener('click', () => {
      document.getElementById('fm-upload-queue').style.display = 'none';
    });
    document.getElementById('fm-btn-delete-sel').addEventListener('click', fmDeleteSelected);
    document.getElementById('fm-btn-rename-sel').addEventListener('click', () => {
      const sel = Array.from(document.querySelectorAll('.fm-row-chk:checked'));
      if (sel.length === 1) fmRename(sel[0].dataset.path, sel[0].dataset.name);
    });
    document.getElementById('fm-search').addEventListener('input', renderFMFileList);

    // Click delegation: sort headers + row-select toggle
    document.getElementById('fm-list').addEventListener('click', (e) => {
      const th = e.target.closest('.fm-sort-col');
      if (th) { fmSetSort(th.dataset.sort); return; }
      if (!e.target.closest('button') && !e.target.closest('.fm-chk-col')) {
        const row = e.target.closest('.fm-row');
        if (row) {
          const chk = row.querySelector('.fm-row-chk');
          if (chk) { chk.checked = !chk.checked; updateFMSelection(); }
        }
      }
    });

    // Checkbox change (select-all or individual row)
    document.getElementById('fm-list').addEventListener('change', (e) => {
      if (e.target.id === 'fm-chk-all') {
        document.querySelectorAll('.fm-row-chk').forEach(c => { c.checked = e.target.checked; });
      }
      updateFMSelection();
    });

    // Right-click context menu
    document.getElementById('fm-list').addEventListener('contextmenu', (e) => {
      const row = e.target.closest('.fm-row');
      if (!row || !fm.siteId) return;
      e.preventDefault();
      fmCtxItem = { path: row.dataset.path, name: row.dataset.name, isDir: row.dataset.isdir === 'true' };
      const chk = row.querySelector('.fm-row-chk');
      if (chk && !chk.checked) { chk.checked = true; updateFMSelection(); }
      showCtxMenu(e.clientX, e.clientY, fmCtxItem.isDir);
    });
    document.addEventListener('click', () => hideCtxMenu());
    document.getElementById('ctx-open').addEventListener('click',    () => { if (fmCtxItem && fmCtxItem.isDir)  fmNavigate(fmCtxItem.path); });
    document.getElementById('ctx-edit').addEventListener('click',    () => { if (fmCtxItem && !fmCtxItem.isDir) fmEditFile(fmCtxItem.path, fmCtxItem.name); });
    document.getElementById('ctx-download').addEventListener('click',() => { if (fmCtxItem && !fmCtxItem.isDir) fmDownload(fmCtxItem.path, fmCtxItem.name); });
    document.getElementById('ctx-rename').addEventListener('click',  () => { if (fmCtxItem) fmRename(fmCtxItem.path, fmCtxItem.name); });
    document.getElementById('ctx-delete').addEventListener('click',  () => { if (fmCtxItem) fmDelete(fmCtxItem.path, fmCtxItem.name); });

    // Drag-and-drop upload onto the card
    const fmCard = document.getElementById('fm-card');
    const dropOv = document.getElementById('fm-drop-overlay');
    let dragN = 0;
    fmCard.addEventListener('dragenter', (e) => { if (!fm.siteId) return; e.preventDefault(); if (++dragN === 1) dropOv.style.display = 'flex'; });
    fmCard.addEventListener('dragleave', () => { if (--dragN <= 0) { dragN = 0; dropOv.style.display = 'none'; } });
    fmCard.addEventListener('dragover',  (e) => e.preventDefault());
    fmCard.addEventListener('drop', async (e) => {
      e.preventDefault(); dragN = 0; dropOv.style.display = 'none';
      const files = Array.from(e.dataTransfer.files);
      if (files.length && fm.siteId) await fmUploadFiles(files);
    });

    // Keyboard shortcuts (active only when on Files page)
    document.addEventListener('keydown', (e) => {
      if (currentPage !== 'files' || !fm.siteId) return;
      const act = document.activeElement;
      if (act && (act.tagName === 'INPUT' || act.tagName === 'TEXTAREA')) return;
      if (e.ctrlKey && e.key === 'a') { e.preventDefault(); fmSelectAll(); }
      if (e.key === 'Escape') fmClearSelection();
      if (e.key === 'Delete') { const s = document.querySelectorAll('.fm-row-chk:checked'); if (s.length) { e.preventDefault(); fmDeleteSelected(); } }
      if (e.key === 'F2') { const s = Array.from(document.querySelectorAll('.fm-row-chk:checked')); if (s.length === 1) { e.preventDefault(); fmRename(s[0].dataset.path, s[0].dataset.name); } }
    });

    document.getElementById('fm-btn-save').addEventListener('click', async () => {
      if (!fm.editingFile) return;
      const content = document.getElementById('fm-editor').value;
      try {
        await PUT(`/files/${fm.siteId}/write`, { path: fm.editingFile, content });
        toast('success', 'File saved', fm.editingFile);
        document.getElementById('fm-editor-card').style.display = 'none';
        fmBrowse(fm.currentPath);
      } catch (err) { toast('error', 'Save failed', err.message); }
    });
    document.getElementById('fm-btn-close-editor').addEventListener('click', () => {
      document.getElementById('fm-editor-card').style.display = 'none';
    });
  }

  function enableFMButtons(en) {
    document.getElementById('fm-btn-upload').disabled = !en;
    document.getElementById('fm-btn-mkdir').disabled  = !en;
    if (!en) {
      const chkAll = document.getElementById('fm-chk-all');
      if (chkAll) { chkAll.checked = false; chkAll.indeterminate = false; }
      document.getElementById('fm-btn-delete-sel').style.display = 'none';
      document.getElementById('fm-btn-rename-sel').style.display = 'none';
    }
  }

  async function fmBrowse(path) {
    fm.currentPath = path;
    updateFMBreadcrumb(path);
    document.getElementById('fm-tbody').innerHTML = '<tr><td colspan="5" class="empty-row"><i class="fa-solid fa-circle-notch fa-spin" style="margin-right:6px"></i>Loading…</td></tr>';
    try {
      const data = await GET(`/files/${fm.siteId}/browse?path=${encodeURIComponent(path)}`);
      fmItems = data.items || [];
      renderFMFileList();
    } catch (err) {
      toast('error', 'Browse error', err.message);
      document.getElementById('fm-tbody').innerHTML = `<tr><td colspan="5" class="empty-row" style="color:var(--red)">${esc(err.message)}</td></tr>`;
    }
  }

  async function fmLoadTree(path) {
    const tree = document.getElementById('fm-tree');
    tree.innerHTML = '';
    try {
      const data = await GET(`/files/${fm.siteId}/browse?path=${encodeURIComponent(path)}`);
      const dirs = data.items.filter(i => i.isDir);
      dirs.forEach(item => {
        const div = document.createElement('div');
        div.className = 'tree-item';
        div.innerHTML = `<i class="fa-solid fa-folder" style="color:var(--yellow)"></i> ${esc(item.name)}`;
        div.addEventListener('click', () => fmNavigate(item.path));
        tree.appendChild(div);
      });
      if (!dirs.length) {
        tree.innerHTML = '<div class="fm-placeholder" style="padding:12px 16px">No sub-folders</div>';
      }
    } catch { /* ignore */ }
  }

  function renderFMFileList() {
    // Reset select-all UI state
    const chkAll = document.getElementById('fm-chk-all');
    if (chkAll) { chkAll.checked = false; chkAll.indeterminate = false; }
    document.getElementById('fm-btn-delete-sel').style.display = 'none';
    document.getElementById('fm-btn-rename-sel').style.display = 'none';

    // Search filter
    const q = (document.getElementById('fm-search').value || '').toLowerCase().trim();
    let items = q ? fmItems.filter(i => i.name.toLowerCase().includes(q)) : [...fmItems];

    // Sort: dirs first, then by selected column
    const { col, dir } = fmSort;
    items.sort((a, b) => {
      if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
      let cmp = 0;
      if (col === 'name')     cmp = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
      if (col === 'size')     cmp = (a.size || 0) - (b.size || 0);
      if (col === 'modified') cmp = new Date(a.modified) - new Date(b.modified);
      return dir === 'asc' ? cmp : -cmp;
    });

    // Update sort arrows in headers
    ['name', 'size', 'modified'].forEach(c => {
      const el = document.getElementById('si-' + c);
      if (el) el.textContent = c === col ? (dir === 'asc' ? '▲' : '▼') : '';
    });

    const tbody = document.getElementById('fm-tbody');
    tbody.innerHTML = items.length
      ? items.map(item => `
          <tr class="fm-row" data-path="${esc(item.path)}" data-name="${esc(item.name)}" data-isdir="${item.isDir}">
            <td class="fm-chk-col" onclick="event.stopPropagation()">
              <input type="checkbox" class="fm-row-chk" data-path="${esc(item.path)}" data-name="${esc(item.name)}" />
            </td>
            <td class="fm-name-cell">
              ${getFileIcon(item.name, item.isDir)}
              ${item.isDir
                ? `<span class="fm-name fm-dir-link" onclick="App.fmNavigate('${esc(item.path)}')">${esc(item.name)}</span>`
                : `<span class="fm-name">${esc(item.name)}</span>`}
            </td>
            <td class="fm-meta-cell">${item.isDir ? '<span style="color:var(--text-3)">—</span>' : fmtSize(item.size)}</td>
            <td class="fm-meta-cell" style="white-space:nowrap">${fmtDate(item.modified)}</td>
            <td class="actions-cell">
              ${item.isDir
                ? `<button class="btn btn-ghost btn-xs" onclick="App.fmNavigate('${esc(item.path)}')" title="Open"><i class="fa-solid fa-folder-open"></i></button>`
                : `<button class="btn btn-ghost btn-xs" onclick="App.fmEditFile('${esc(item.path)}','${esc(item.name)}')" title="Edit"><i class="fa-solid fa-pen"></i></button>
                   <button class="btn btn-ghost btn-xs" onclick="App.fmDownload('${esc(item.path)}','${esc(item.name)}')" title="Download"><i class="fa-solid fa-download"></i></button>`}
              <button class="btn btn-ghost btn-xs" onclick="App.fmRename('${esc(item.path)}','${esc(item.name)}')" title="Rename (F2)"><i class="fa-solid fa-i-cursor"></i></button>
              <button class="btn btn-danger btn-xs" onclick="App.fmDelete('${esc(item.path)}','${esc(item.name)}')" title="Delete (Del)"><i class="fa-solid fa-trash"></i></button>
            </td>
          </tr>`).join('')
      : `<tr><td colspan="5" class="empty-row">${q ? 'No files match "' + esc(q) + '".' : 'This folder is empty.'}</td></tr>`;

    updateFMStatusBar(items, q);
  }

  async function fmNavigate(path) {
    await fmBrowse(path);
    await fmLoadTree(path);
  }

  function updateFMBreadcrumb(path) {
    const bc = document.getElementById('fm-breadcrumb');
    const parts = path.split('/').filter(Boolean);
    let html = `<span class="crumb root" onclick="App.fmNavigate('/')"><i class="fa-solid fa-house-chimney"></i> Root</span>`;
    let built = '';
    parts.forEach(p => {
      built += '/' + p;
      const snap = built;
      html += `<span class="crumb-sep">›</span><span class="crumb" onclick="App.fmNavigate('${esc(snap)}')">${esc(p)}</span>`;
    });
    bc.innerHTML = html;
  }

  async function fmEditFile(filePath, filename) {
    try {
      const data = await GET(`/files/${fm.siteId}/read?path=${encodeURIComponent(filePath)}`);
      if (data.encoding === 'base64') {
        toast('warning', 'Binary file', 'Binary files cannot be edited in the browser editor');
        return;
      }
      fm.editingFile = filePath;
      document.getElementById('fm-editor-filename').textContent = filename;
      document.getElementById('fm-editor').value = data.content;
      document.getElementById('fm-editor-card').style.display = 'block';
      document.getElementById('fm-editor').scrollIntoView({ behavior: 'smooth' });
    } catch (err) { toast('error', 'Open error', err.message); }
  }

  async function fmDelete(filePath, name) {
    const ok = await confirm('Delete', `Delete <strong>${esc(name)}</strong>? This cannot be undone.`, 'Delete', 'btn-danger');
    if (!ok) return;
    try {
      await DEL(`/files/${fm.siteId}/delete`, { path: filePath });
      toast('success', 'Deleted', name);
      fmBrowse(fm.currentPath);
    } catch (err) { toast('error', 'Delete failed', err.message); }
  }

  // Upload multiple files with progress panel
  async function fmUploadFiles(files) {
    const panel  = document.getElementById('fm-upload-queue');
    const listEl = document.getElementById('fm-upload-list');

    listEl.innerHTML = files.map((f, i) => `
      <div class="uq-row" id="uq-row-${i}">
        <i class="fa-solid fa-circle-notch fa-spin uq-icon" id="uq-icon-${i}" style="color:var(--indigo)"></i>
        <span class="uq-name">${esc(f.name)}</span>
        <span class="uq-size">${fmtSize(f.size)}</span>
        <span class="uq-status" id="uq-status-${i}">Waiting…</span>
      </div>`).join('');
    panel.style.display = 'block';

    let success = 0, failed = 0;
    for (let i = 0; i < files.length; i++) {
      const iconEl   = document.getElementById(`uq-icon-${i}`);
      const statusEl = document.getElementById(`uq-status-${i}`);
      statusEl.textContent = 'Uploading…';
      try {
        const fd = new FormData();
        fd.append('file', files[i]);
        fd.append('destPath', fm.currentPath);
        await api('POST', `/files/${fm.siteId}/upload`, fd);
        iconEl.className = 'fa-solid fa-circle-check uq-icon';
        iconEl.style.color = 'var(--green)';
        statusEl.textContent = 'Done';
        statusEl.style.color = 'var(--green)';
        success++;
      } catch (err) {
        iconEl.className = 'fa-solid fa-circle-xmark uq-icon';
        iconEl.style.color = 'var(--red)';
        statusEl.textContent = err.message;
        statusEl.style.color = 'var(--red)';
        failed++;
      }
    }

    if (failed === 0) toast('success', 'Upload complete', `${success} file${success > 1 ? 's' : ''} uploaded`);
    else toast('warning', 'Upload done', `${success} OK, ${failed} failed`);

    await fmBrowse(fm.currentPath);
    if (failed === 0) setTimeout(() => { panel.style.display = 'none'; }, 3500);
  }

  // Update selection counter + toggle delete-sel button
  function updateFMSelection() {
    const checked = document.querySelectorAll('.fm-row-chk:checked');
    const total   = document.querySelectorAll('.fm-row-chk');
    const btn = document.getElementById('fm-btn-delete-sel');
    document.getElementById('fm-sel-count').textContent = checked.length;
    btn.style.display = checked.length > 0 ? '' : 'none';
    const chkAll = document.getElementById('fm-chk-all');
    if (chkAll) {
      chkAll.indeterminate = checked.length > 0 && checked.length < total.length;
      chkAll.checked = total.length > 0 && checked.length === total.length;
    }
  }

  // Bulk-delete all checked items
  async function fmDeleteSelected() {
    const checked = Array.from(document.querySelectorAll('.fm-row-chk:checked'));
    if (!checked.length) return;
    const names = checked.map(c => esc(c.dataset.name)).join(', ');
    const ok = await confirm(
      `Delete ${checked.length} item${checked.length > 1 ? 's' : ''}`,
      `Permanently delete <strong>${checked.length}</strong> item(s)?<br><small style="color:var(--text-3)">${names}</small>`,
      'Delete All', 'btn-danger'
    );
    if (!ok) return;
    let failed = 0;
    for (const chk of checked) {
      try { await DEL(`/files/${fm.siteId}/delete`, { path: chk.dataset.path }); }
      catch { failed++; }
    }
    if (failed > 0) toast('warning', 'Delete', `${checked.length - failed} deleted, ${failed} failed`);
    else toast('success', 'Deleted', `${checked.length} item${checked.length > 1 ? 's' : ''} removed`);
    fmBrowse(fm.currentPath);
  }

  function fmSetSort(col) {
    if (fmSort.col === col) fmSort.dir = fmSort.dir === 'asc' ? 'desc' : 'asc';
    else { fmSort.col = col; fmSort.dir = 'asc'; }
    renderFMFileList();
  }

  function fmSelectAll() {
    document.querySelectorAll('.fm-row-chk').forEach(c => { c.checked = true; });
    updateFMSelection();
  }

  function fmClearSelection() {
    document.querySelectorAll('.fm-row-chk').forEach(c => { c.checked = false; });
    const chkAll = document.getElementById('fm-chk-all');
    if (chkAll) { chkAll.checked = false; chkAll.indeterminate = false; }
    updateFMSelection();
  }

  function updateFMStatusBar(items, q) {
    const statusEl = document.getElementById('fm-status-text');
    if (!statusEl) return;
    const dirs  = items.filter(i => i.isDir).length;
    const files = items.length - dirs;
    let msg = '';
    if (q) msg = `${items.length} result${items.length !== 1 ? 's' : ''} for "${q}"`;
    else msg = `${dirs} folder${dirs !== 1 ? 's' : ''}, ${files} file${files !== 1 ? 's' : ''}`;
    statusEl.textContent = msg;
  }

  async function fmRename(filePath, oldName) {
    const newName = prompt('Rename to:', oldName);
    if (!newName || newName === oldName) return;
    const dirPart = filePath.substring(0, filePath.lastIndexOf('/') + 1);
    const newPath = dirPart + newName;
    try {
      await PUT(`/files/${fm.siteId}/rename`, { from: filePath, to: newPath });
      toast('success', 'Renamed', `${oldName} → ${newName}`);
      fmBrowse(fm.currentPath);
    } catch (err) { toast('error', 'Rename failed', err.message); }
  }

  function fmDownload(filePath, name) {
    const a = document.createElement('a');
    a.href     = `/api/files/${fm.siteId}/download?path=${encodeURIComponent(filePath)}`;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  async function fmMakeDir() {
    const name = prompt('New folder name:');
    if (!name) return;
    const cleanName = name.replace(/[/\\:*?"<>|]/g, '_');
    const newPath = (fm.currentPath === '/' ? '' : fm.currentPath) + '/' + cleanName;
    try {
      await POST(`/files/${fm.siteId}/mkdir`, { path: newPath });
      toast('success', 'Folder created', cleanName);
      fmBrowse(fm.currentPath);
    } catch (err) { toast('error', 'Error', err.message); }
  }

  function showCtxMenu(x, y, isDir) {
    const menu = document.getElementById('fm-ctx-menu');
    document.getElementById('ctx-open').style.display     = isDir  ? 'flex' : 'none';
    document.getElementById('ctx-edit').style.display     = !isDir ? 'flex' : 'none';
    document.getElementById('ctx-download').style.display = !isDir ? 'flex' : 'none';
    menu.style.left    = x + 'px';
    menu.style.top     = y + 'px';
    menu.style.display = 'block';
    // Keep menu within viewport
    const rect = menu.getBoundingClientRect();
    if (rect.right  > window.innerWidth)  menu.style.left = (x - rect.width)  + 'px';
    if (rect.bottom > window.innerHeight) menu.style.top  = (y - rect.height) + 'px';
  }

  function hideCtxMenu() {
    const menu = document.getElementById('fm-ctx-menu');
    if (menu) menu.style.display = 'none';
  }

  // ─── Activity ─────────────────────────────────────────────────
  async function loadActivity() {
    try {
      const data = await GET('/activity?limit=200');
      const tbody = document.getElementById('tbody-activity');
      tbody.innerHTML = data.length
        ? data.map(e => `
            <tr>
              <td style="font-size:12px;white-space:nowrap;color:var(--text-3)">${esc(fmtDate(e.timestamp))}</td>
              <td>${levelBadge(e.level)}</td>
              <td><code>${esc(e.category)}</code></td>
              <td>${esc(e.message)}</td>
            </tr>`).join('')
        : '<tr><td colspan="4" class="empty-row">No activity recorded yet.</td></tr>';
    } catch (err) { toast('error', 'Activity', err.message); }
  }

  async function clearActivityLog() {
    const ok = await confirm('Clear Log', 'This will permanently clear all activity records.', 'Clear', 'btn-danger');
    if (!ok) return;
    try {
      await DEL('/activity');
      toast('success', 'Log cleared', '');
      loadActivity();
    } catch (err) { toast('error', 'Error', err.message); }
  }

  // ─── Settings ─────────────────────────────────────────────────
  async function loadSettings() {
    try {
      const cfg = await GET('/config');
      const s = cfg.settings;
      document.getElementById('cfg-max-versions').value = s.maxVersions ?? 10;
      document.getElementById('cfg-auto-backup').checked = !!s.autoBackup;
      document.getElementById('cfg-stop-pool').checked   = !!s.stopPoolBeforeDeploy;
      document.getElementById('cfg-iis-reset').checked   = !!s.iisResetOnDeploy;
    } catch (err) { toast('error', 'Settings', err.message); }
  }

  async function saveSettings(e) {
    e.preventDefault();
    try {
      await PUT('/config/settings', {
        maxVersions:        parseInt(document.getElementById('cfg-max-versions').value) || 10,
        autoBackup:         document.getElementById('cfg-auto-backup').checked,
        stopPoolBeforeDeploy: document.getElementById('cfg-stop-pool').checked,
        iisResetOnDeploy:   document.getElementById('cfg-iis-reset').checked,
      });
      toast('success', 'Settings saved', '');
    } catch (err) { toast('error', 'Save failed', err.message); }
  }

  // ─── User Management ──────────────────────────────────────────

  async function loadUsers() {
    try {
      const users = await GET('/auth/users');
      const tbody = document.getElementById('tbody-users');
      const roleBadge = r => r === 'superadmin'
        ? '<span class="badge badge-running"><i class="fa-solid fa-crown"></i> Super Admin</span>'
        : '<span class="badge badge-info"><i class="fa-solid fa-user"></i> User</span>';
      tbody.innerHTML = users.length
        ? users.map(u => `
            <tr>
              <td><code>${esc(u.username)}</code></td>
              <td>${esc(u.displayName || '—')}</td>
              <td>${roleBadge(u.role)}</td>
              <td style="font-size:12px;color:var(--text-3)">${esc(fmtDate(u.createdAt))}</td>
              <td style="font-size:12px;color:var(--text-3)">${u.lastLogin ? esc(fmtDate(u.lastLogin)) : '—'}</td>
              <td>
                <div style="display:flex;gap:6px">
                  <button class="btn btn-xs btn-ghost" onclick="App.openEditUser('${esc(u.id)}','${esc(u.username)}','${esc(u.displayName||'')}','${esc(u.role)}')">
                    <i class="fa-solid fa-pen"></i>
                  </button>
                  ${currentUser && u.id !== currentUser.id ? `<button class="btn btn-xs btn-danger" onclick="App.deleteUser('${esc(u.id)}','${esc(u.username)}')"><i class="fa-solid fa-trash"></i></button>` : ''}
                </div>
              </td>
            </tr>`).join('')
        : '<tr><td colspan="6" class="empty-row">No users found.</td></tr>';
    } catch (err) { toast('error', 'Users', err.message); }
  }

  function openAddUser() {
    document.getElementById('modal-user-title').textContent = 'Add User';
    document.getElementById('user-edit-id').value = '';
    document.getElementById('user-username').value = '';
    document.getElementById('user-username').disabled = false;
    document.getElementById('user-displayname').value = '';
    document.getElementById('user-role').value = 'user';
    document.getElementById('user-password').value = '';
    document.getElementById('user-pass-hint').style.display = 'none';
    document.getElementById('user-pass-label').innerHTML = 'Password <span class="required">*</span>';
    document.getElementById('modal-user').style.display = 'flex';
  }

  function openEditUser(id, username, displayName, role) {
    document.getElementById('modal-user-title').textContent = 'Edit User';
    document.getElementById('user-edit-id').value = id;
    document.getElementById('user-username').value = username;
    document.getElementById('user-username').disabled = true;
    document.getElementById('user-displayname').value = displayName;
    document.getElementById('user-role').value = role;
    document.getElementById('user-password').value = '';
    document.getElementById('user-pass-hint').style.display = 'inline';
    document.getElementById('user-pass-label').textContent = 'Password';
    document.getElementById('modal-user').style.display = 'flex';
  }

  async function saveUser() {
    const id          = document.getElementById('user-edit-id').value;
    const username    = document.getElementById('user-username').value.trim();
    const displayName = document.getElementById('user-displayname').value.trim();
    const role        = document.getElementById('user-role').value;
    const password    = document.getElementById('user-password').value;

    if (!id && (!username || !password)) {
      toast('warning', 'User', 'Username and password are required');
      return;
    }

    try {
      if (id) {
        const body = { displayName, role };
        if (password) body.password = password;
        await PUT(`/auth/users/${id}`, body);
        toast('success', 'User', 'User updated');
      } else {
        await api('POST', '/auth/users', { username, displayName, role, password });
        toast('success', 'User', 'User created');
      }
      closeModal('modal-user');
      loadUsers();
    } catch (err) { toast('error', 'User', err.message); }
  }

  async function deleteUser(id, username) {
    const ok = await confirm(
      'Delete User',
      `Permanently delete user <strong>${esc(username)}</strong>? This cannot be undone.`,
      'Delete', 'btn-danger'
    );
    if (!ok) return;
    try {
      await DEL(`/auth/users/${id}`);
      toast('success', 'User deleted', '');
      loadUsers();
    } catch (err) { toast('error', 'Delete failed', err.message); }
  }

  // ─── Clock ───────────────────────────────────────────────────
  function startClock() {
    const el = document.getElementById('topbar-time');
    const tick = () => { el.textContent = new Date().toLocaleTimeString(); };
    tick();
    setInterval(tick, 1000);
  }

  // ─── Auth init ────────────────────────────────────────────────
  function initAuth() {
    const token = localStorage.getItem('dp_token');
    if (!token) { window.location.href = '/login'; return false; }

    try {
      const stored = localStorage.getItem('dp_user');
      currentUser  = stored ? JSON.parse(stored) : null;
    } catch { currentUser = null; }

    if (currentUser) {
      document.getElementById('topbar-username').textContent =
        currentUser.displayName || currentUser.username;
    }

    // Show superadmin-only UI
    if (currentUser && currentUser.role === 'superadmin') {
      document.querySelectorAll('.superadmin-only').forEach(el => el.style.display = '');
      const usersPage = document.getElementById('page-users');
      if (usersPage) usersPage.style.display = '';
    }

    // Logout handler
    document.getElementById('btn-logout').addEventListener('click', () => {
      localStorage.removeItem('dp_token');
      localStorage.removeItem('dp_user');
      window.location.href = '/login';
    });

    return true;
  }

  // ─── Init ────────────────────────────────────────────────────
  function init() {
    if (!initAuth()) return;
    connectWS();
    startClock();
    initDeployForm();
    initFileManager();

    // Sidebar nav
    document.querySelectorAll('.nav-item[data-page]').forEach(el => {
      el.addEventListener('click', (e) => {
        e.preventDefault();
        navigate(el.dataset.page);
      });
    });

    // Sidebar toggle
    document.getElementById('sidebar-toggle').addEventListener('click', () => {
      document.getElementById('app').classList.toggle('sidebar-collapsed');
    });

    // IIS reset buttons
    document.getElementById('btn-iis-reset-main').addEventListener('click', triggerIISReset);
    document.getElementById('btn-iis-reset-topbar').addEventListener('click', triggerIISReset);

    // Managed sites
    document.getElementById('btn-add-site').addEventListener('click', openAddSite);
    document.getElementById('btn-import-iis').addEventListener('click', importFromIIS);
    document.getElementById('modal-site-save').addEventListener('click', saveSite);

    // Rollback confirm btn
    document.getElementById('btn-confirm-rollback').addEventListener('click', executeRollback);

    // Versions filter
    document.getElementById('ver-site-filter').addEventListener('change', renderVersions);

    // Clear activity log
    document.getElementById('btn-clear-log').addEventListener('click', clearActivityLog);

    // Settings form
    document.getElementById('settings-form').addEventListener('submit', saveSettings);

    // User management
    document.getElementById('btn-add-user').addEventListener('click', openAddUser);
    document.getElementById('modal-user-save').addEventListener('click', saveUser);

    // Load initial page
    navigate('dashboard');
  }

  return {
    init,
    navigate,
    // IIS
    loadIISSites,
    loadIISPools,
    iisControlSite,
    iisControlPool,
    // Versions / rollback
    openRollback,
    // File manager
    fmNavigate,
    fmEditFile,
    fmDelete,
    fmDeleteSelected,
    fmRename,
    fmDownload,
    // Sites
    openAddSite,
    openEditSite,
    deleteSite,
    importFromIIS,
    // Users
    openEditUser,
    deleteUser,
    // Misc
    clearLiveFeed,
    closeModal,
    closeSiteModal,
  };

})();

document.addEventListener('DOMContentLoaded', () => App.init());
