// app-data.js — Database, file manager, SMTP, users, settings, security, scheduler

// ── Database Manager ──────────────────────────────────────
var dbActiveEngine  = '';
var dbActiveTab     = 'databases';
var tvDb = '', tvTable = '', tvPage = 1;
var dbList          = [];
var curQueryDb      = '';
var curDbPermUser   = '';
var _dbConnected    = { sqlserver: false, mysql: false, postgres: false };
var _dbStatusCache  = null;   // cache engine status so we don't re-check on tab switch
var _dbCredsCache   = {};     // cache per engine so form doesn't flicker
var _showConnForm   = {};     // whether "change connection" is expanded per engine
var _serverStart = null;

var _dbDownloadLinks = {
  sqlserver: 'https://www.microsoft.com/en-us/sql-server/sql-server-downloads',
  mysql:     'https://dev.mysql.com/downloads/mysql/',
  postgres:  'https://www.postgresql.org/download/windows/'
};

var _cache = {
  sites:    { data: null, ts: 0 },
  apppools: { data: null, ts: 0 },
  services: { data: null, ts: 0 },
  dotnet:   { data: null, ts: 0 },
  domains:  { data: null, ts: 0 },   
  iissites: { data: null, ts: 0 },   
  winsvc:   { data: null, ts: 0 },  
  docker:   { data: null, ts: 0 },   // ← ADD
  users:    { data: null, ts: 0 },   // ← ADD
  settings: { data: null, ts: 0 },   // ← ADD
  dbstatus: { data: null, ts: 0 },
  ssl:       { data: null, ts: 0 },   // ← ADD
  smtp:      { data: null, ts: 0 },   // ← ADD
  scheduler: { data: null, ts: 0 },   // ← ADD
  security:  { data: null, ts: 0 },   // ← ADD
  traffic:   { data: null, ts: 0 },   // ← ADD
  backups: { data: null, ts: 0 }, 
  redirects: { data: null, ts: 0 } 
};
var _CACHE_TTL = Infinity; // 30 seconds

function cacheGet(key) {
  var c = _cache[key];
  return (c && c.data && (Date.now() - c.ts) < _CACHE_TTL) ? c.data : null;
}
function cacheSet(key, data) {
  _cache[key] = { data: data, ts: Date.now() };
}
function cacheClear(key) {
  if (_cache[key]) _cache[key] = { data: null, ts: 0 };
}

// ── 1. DATABASES ───────────────────────────────────────────
function loadDatabase(c) {
  // Skeleton: 3 engine cards + empty panel
  c.innerHTML =
    '<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap">' +
      [1,2,3].map(function() {
        return '<div style="padding:6px 20px;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc">' +
          '<div class="sk-line" style="width:80px;height:14px"></div>' +
        '</div>';
      }).join('') +
      '<div style="padding:3px 10px;border-radius:6px;border:1px solid #e2e8f0;background:#f8fafc">' +
        '<div class="sk-line" style="width:60px;height:14px"></div>' +
      '</div>' +
    '</div>' +
    '<div id="db-engine-panel">' +
      skeletonCard('Databases', ['Name','State','Size','Actions'], 4,
        '<button class="btn-neutral" disabled>↻ Refresh</button>') +
    '</div>';

var statusPromise = cacheGet('dbstatus')
  ? Promise.resolve(cacheGet('dbstatus'))
  : api('/api/db/status').then(function(st){ cacheSet('dbstatus', st); return st; });

  statusPromise.then(function(st) {
    var engines = [
      { key:'sqlserver', icon:'🗄️', label:'SQL Server' },
      { key:'mysql',     icon:'🐬', label:'MySQL'      },
      { key:'postgres',  icon:'🐘', label:'PostgreSQL' }
    ];
    var installed    = engines.filter(function(e){ return st[e.key]; });
    var notInstalled = engines.filter(function(e){ return !st[e.key]; });

    if (!dbActiveEngine && installed.length) dbActiveEngine = installed[0].key;

    var engTabs = installed.map(function(e) {
      var dot    = _dbConnected[e.key]
        ? '<span style="color:#16a34a;font-size:10px;margin-left:4px">●</span>'
        : '<span style="color:#cbd5e1;font-size:10px;margin-left:4px">○</span>';
      var active = e.key === dbActiveEngine;
      return '<button onclick="dbSwitchEngine(\'' + e.key + '\')" style="padding:6px 14px;border-radius:6px;border:1px solid ' +
        (active?'#3b82f6':'#e2e8f0') + ';background:' + (active?'#eff6ff':'#fff') + ';color:' +
        (active?'#1d4ed8':'#64748b') + ';cursor:pointer;font-size:13px;font-weight:' + (active?'500':'400') + '">' +
        e.icon + ' ' + e.label + dot + '</button>';
    }).join('');

    var installCards = notInstalled.map(function(e) {
      return '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;' +
        'background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;margin-bottom:8px">' +
        '<span style="font-size:13px;color:#64748b">' + e.icon + ' ' + e.label + ' — not installed</span>' +
        '<div style="display:flex;gap:6px">' +
          '<button class="btn-primary" id="instbtn-' + e.key + '" style="font-size:11px;padding:4px 10px" ' +
            'onclick="installEng(\'' + e.key + '\',\'' + e.label + '\')">Install</button>' +
          '<a href="' + _dbDownloadLinks[e.key] + '" target="_blank" class="btn-neutral" ' +
            'style="text-decoration:none;font-size:11px;padding:4px 10px">⬇ Download</a>' +
        '</div></div>';
    }).join('');

    c.innerHTML =
      '<div style="display:flex;align-items:center;gap:8px;margin-bottom:' + (installCards?'12px':'16px') + ';flex-wrap:wrap">' +
        engTabs +
        '<button class="btn-neutral" onclick="cacheClear(\'dbstatus\');loadDatabase(el(\'content\'))">↻ Refresh</button>' +
      '</div>' +
      (installCards ? '<div style="margin-bottom:14px">' + installCards + '</div>' : '') +
      '<div id="db-engine-panel"></div>';

    if (installed.length) renderDbEngine(dbActiveEngine, el('db-engine-panel'));
  }).catch(function() { c.innerHTML = '<div class="err-box">Cannot reach backend.</div>'; });
}

function dbSwitchEngine(eng) {
  dbActiveEngine = eng;
  dbActiveTab    = 'databases';
  loadDatabase(el('content'));
}

// ── Engine panel with tabs ─────────────────────────────────
function renderDbEngine(eng, panel) {
  var tabs = [
    { id:'databases',   label:'Databases'    },
    { id:'tableviewer', label:'Table Viewer' },
    { id:'query',       label:'Query Runner' },
    { id:'users',       label:'DB Users', sqlonly: true }
  ].filter(function(t){ return !t.sqlonly || eng === 'sqlserver'; });

  var tabBar = '<div class="tabs" style="margin-bottom:0;border-bottom:1px solid #e2e8f0;margin-bottom:16px">' +
    tabs.map(function(t) {
      return '<div class="tab' + (t.id===dbActiveTab?' active':'') + '" onclick="dbSwitchTab(\'' + t.id + '\')">' + t.label + '</div>';
    }).join('') + '</div>';

  panel.innerHTML = tabBar + '<div id="db-tab-content"></div>';
  dbLoadTab(dbActiveTab, eng);
}

function dbSwitchTab(tab) {
  dbActiveTab = tab;
  document.querySelectorAll('.tab').forEach(function(t) {
    var map = { databases:'Databases', tableviewer:'Table Viewer', query:'Query Runner', users:'DB Users' };
    if (Object.values(map).includes(t.textContent.trim()))
      t.classList.toggle('active', t.textContent.trim() === (map[tab]||''));
  });
  dbLoadTab(tab, dbActiveEngine);
}

function dbLoadTab(tab, eng) {
  var tc = el('db-tab-content'); if (!tc) return;
  if (tab === 'databases')   dbLoadDatabases(tc, eng);
  if (tab === 'tableviewer') dbLoadTableViewer(tc, eng);
  if (tab === 'query')       dbLoadQueryRunner(tc, eng);
  if (tab === 'users')       loadDbUsers(tc);
}

// ── Connection bar (compact when connected) ────────────────
function dbLoadDatabases(tc, eng) {
  // Show cached creds immediately, fetch in background only if not cached
  var creds = _dbCredsCache[eng];
  if (creds) {
    renderDbConnForm(tc, eng, creds);
    if (_dbConnected[eng]) dbFetchDatabases(eng);
  } else {
    tc.innerHTML = '<div class="loading" style="font-size:12px">Loading connection info...</div>';
    api('/api/db/credentials/' + eng).then(function(c2) {
      _dbCredsCache[eng] = c2;
      renderDbConnForm(tc, eng, c2);
      if (_dbConnected[eng]) dbFetchDatabases(eng);
    }).catch(function() {
      _dbCredsCache[eng] = {};
      renderDbConnForm(tc, eng, {});
    });
  }
}

function renderDbConnForm(tc, eng, creds) {
  var connected = _dbConnected[eng];
  var showForm  = !connected || _showConnForm[eng];
  var def = { sqlserver:{host:'localhost',port:1433,user:'sa'}, mysql:{host:'localhost',port:3306,user:'root'}, postgres:{host:'localhost',port:5432,user:'postgres'} }[eng] || {};

  var connBar = connected && !showForm
    // ── Compact connected bar ──────────────────────────────
    ? '<div style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:#f0fdf4;' +
        'border:1px solid #86efac;border-radius:8px;margin-bottom:14px">' +
        '<span style="color:#16a34a;font-size:16px">●</span>' +
        '<div><div style="font-size:13px;font-weight:500;color:#166534">Connected to ' + eng + '</div>' +
        '<div style="font-size:11px;color:#16a34a">' + (creds.host||def.host) + ':' + (creds.port||def.port) + ' as ' + (creds.user||def.user) + '</div></div>' +
        '<button class="btn-neutral" style="margin-left:auto;font-size:11px" onclick="_showConnForm[\'' + eng + '\']=true;dbLoadDatabases(el(\'db-tab-content\'),\'' + eng + '\')">Change connection</button>' +
        '<button class="btn-neutral" style="font-size:11px" onclick="dbDisconnect(\'' + eng + '\')">Disconnect</button>' +
      '</div>'
    // ── Full connection form ───────────────────────────────
    : '<div style="background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0;padding:14px;margin-bottom:14px">' +
        '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">' +
          (connected
            ? '<span style="color:#16a34a;font-size:16px">●</span><span style="font-size:13px;font-weight:500;color:#166534">Connected</span>' +
              '<button class="btn-neutral" style="margin-left:auto;font-size:11px" onclick="_showConnForm[\'' + eng + '\']=false;dbLoadDatabases(el(\'db-tab-content\'),\'' + eng + '\')">Cancel</button>'
            : '<span style="color:#94a3b8;font-size:16px">○</span><span style="font-size:13px;font-weight:500;color:#64748b">Connect to ' + eng + '</span>') +
        '</div>' +
        '<div style="display:grid;grid-template-columns:1fr 90px 1fr 1fr;gap:8px;margin-bottom:10px">' +
          '<div><label style="font-size:11px;color:#64748b;display:block;margin-bottom:3px">Host</label>' +
            '<input id="dc-host" value="' + (creds.host||def.host) + '" style="width:100%;padding:6px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px" /></div>' +
          '<div><label style="font-size:11px;color:#64748b;display:block;margin-bottom:3px">Port</label>' +
            '<input id="dc-port" type="number" value="' + (creds.port||def.port) + '" style="width:100%;padding:6px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px" /></div>' +
          '<div><label style="font-size:11px;color:#64748b;display:block;margin-bottom:3px">Username</label>' +
            '<input id="dc-user" value="' + (creds.user||def.user) + '" style="width:100%;padding:6px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px" /></div>' +
          '<div><label style="font-size:11px;color:#64748b;display:block;margin-bottom:3px">Password</label>' +
            '<input id="dc-pass" type="password" placeholder="' + (creds.hasPass?'Saved — leave blank to keep':'No password set') + '" ' +
            'style="width:100%;padding:6px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px" /></div>' +
        '</div>' +
        '<div style="display:flex;align-items:center;gap:8px">' +
          '<button class="btn-primary" onclick="dbConnect(\'' + eng + '\')">Connect</button>' +
          '<button class="btn-neutral" onclick="dbTestConnection(\'' + eng + '\')">Test connection</button>' +
          '<span id="dc-msg" style="font-size:12px"></span>' +
        '</div>' +
      '</div>';

  var listArea = connected
    ? '<div id="db-list-area"><div class="loading" style="font-size:12px">Loading databases...</div></div>'
    : '<div id="db-list-area"><div style="text-align:center;padding:40px 0;color:#94a3b8;font-size:13px">' +
        'Connect above to see and manage databases.</div></div>';

  tc.innerHTML = connBar + listArea;
  if (connected) dbFetchDatabases(eng);
}

function dbConnect(eng) {
  var host = val('dc-host'), port = parseInt(val('dc-port')),
      user = val('dc-user'), pass = el('dc-pass').value;
  var msg = el('dc-msg');
  msg.textContent = 'Connecting...'; msg.style.color = '#64748b';
  api('/api/db/connect', 'POST', { engine:eng, host:host, port:port, user:user, pass:pass }).then(function(r) {
    if (r.ok) {
      _dbConnected[eng] = true;
      _dbCredsCache[eng] = { host:host, port:port, user:user, hasPass: !!(pass || _dbCredsCache[eng] && _dbCredsCache[eng].hasPass) };
      _showConnForm[eng] = false;
      _dbStatusCache = null;
      dbLoadDatabases(el('db-tab-content'), eng);
      loadDatabase(el('content')); // refresh engine dots
    } else {
      msg.textContent = '✗ ' + (r.error || 'Connection failed');
      msg.style.color = '#dc2626';
    }
  }).catch(function(e) { msg.textContent = '✗ ' + e.message; msg.style.color = '#dc2626'; });
}

function dbTestConnection(eng) {
  var host = val('dc-host'), port = parseInt(val('dc-port')),
      user = val('dc-user'), pass = el('dc-pass').value;
  var msg = el('dc-msg');
  msg.textContent = 'Testing...'; msg.style.color = '#64748b';
  api('/api/db/test-connection', 'POST', { engine:eng, host:host, port:port, user:user, pass:pass }).then(function(r) {
    if (r.ok) { msg.textContent = '✓ Connection successful!'; msg.style.color = '#16a34a'; }
    else       { msg.textContent = '✗ ' + (r.error || 'Failed');  msg.style.color = '#dc2626'; }
  }).catch(function(e) { msg.textContent = '✗ ' + e.message; msg.style.color = '#dc2626'; });
}

function dbDisconnect(eng) {
  _dbConnected[eng]  = false;
  _showConnForm[eng] = false;
  _dbStatusCache     = null;
  dbLoadDatabases(el('db-tab-content'), eng);
  setTimeout(function(){ loadDatabase(el('content')); }, 100);
}

// ── Database list ──────────────────────────────────────────
function dbFetchDatabases(eng) {
  var area = el('db-list-area'); if (!area) return;
  area.innerHTML = '<div class="loading" style="font-size:12px;padding:10px 0">Loading databases...</div>';
  var ep = eng==='mysql' ? '/api/db/mysql/databases'
         : eng==='postgres' ? '/api/db/pg/databases'
         : '/api/db/databases';
  api(ep).then(function(dbs) {
    if (dbs.error) {
      _dbConnected[eng] = false;
      area.innerHTML = '<div class="err-box">' + dbs.error + '</div>';
      return;
    }
    if (!Array.isArray(dbs)) dbs = [dbs];
    dbList = dbs;

    var ths = eng==='sqlserver'
      ? '<th>Name</th><th>State</th><th>Size</th><th>Actions</th>'
      : '<th>Name</th><th>Actions</th>';

    var rows = dbs.map(function(d) {
      var name = d.name || d.Name || '–';
      var extra = eng==='sqlserver'
        ? '<td>' + (d.state_desc||'–') + '</td><td style="font-size:12px;color:#64748b">' + ((d.size_mb||0) + ' MB') + '</td>'
        : '';
      var safeName = name.replace(/'/g, "\\'");
      return '<tr>' +
        '<td class="b">' + name + '</td>' + extra +
        '<td style="white-space:nowrap">' +
          '<button class="btn-neutral" style="margin-left:0" onclick="dbOpenTableViewer(\'' + safeName + '\')">Browse</button>' +
          '<button class="btn-neutral" onclick="dbOpenQuery(\'' + safeName + '\')">Query</button>' +
          (eng==='sqlserver'
            ? '<button class="btn-neutral" onclick="openRenameDb(\'' + safeName + '\')">Rename</button>' : '') +
          '<button class="btn-danger" onclick="dbDropDb(\'' + safeName + '\')">Drop</button>' +
        '</td>' +
      '</tr>';
    }).join('');

    area.innerHTML =
      '<div style="display:flex;gap:8px;margin-bottom:12px">' +
        '<input id="new-db-name" placeholder="New database name" style="flex:1;padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;outline:none" />' +
        '<button class="btn-primary" onclick="dbCreateDb()">+ Create</button>' +
        '<button class="btn-neutral" onclick="dbFetchDatabases(\'' + eng + '\')">↻</button>' +
      '</div>' +
      (rows
        ? '<table><thead><tr>' + ths + '</tr></thead><tbody>' + rows + '</tbody></table>'
        : '<p style="color:#94a3b8;font-size:13px">No user databases found.</p>');
  }).catch(function(e) {
    area.innerHTML = '<div class="err-box">Error: ' + e.message + '</div>';
  });
}

function dbCreateDb() {
  var n = (el('new-db-name') || {value:''}).value.trim();
  if (!n) { alert('Enter a database name'); return; }
  var ep = dbActiveEngine==='mysql' ? '/api/db/mysql/databases'
         : dbActiveEngine==='postgres' ? '/api/db/pg/databases'
         : '/api/db/databases';
  api(ep, 'POST', { name:n }).then(function(r) {
    if (r.error) { alert(r.error); return; }
    dbFetchDatabases(dbActiveEngine);
  });
}

function dbDropDb(name) {
  if (!confirm('Drop database "' + name + '"? This CANNOT be undone.')) return;
  var ep = dbActiveEngine==='mysql' ? '/api/db/mysql/databases/'
         : dbActiveEngine==='postgres' ? '/api/db/pg/databases/'
         : '/api/db/databases/';
  api(ep + encodeURIComponent(name), 'DELETE').then(function(r) {
    if (r.error) { alert(r.error); return; }
    dbFetchDatabases(dbActiveEngine);
  });
}

// ── Rename database (SQL Server only) ─────────────────────
function openRenameDb(name) {
  var newName = prompt('Rename database "' + name + '" to:', name);
  if (!newName || newName === name) return;
  if (!/^[a-zA-Z0-9_]+$/.test(newName)) { alert('Invalid name — letters, numbers and underscore only'); return; }
  api('/api/db/query', 'POST', { query: "ALTER DATABASE [" + name + "] MODIFY NAME = [" + newName + "]", database: 'master' })
    .then(function(r) {
      if (r.error) { alert('Rename failed: ' + r.error); return; }
      dbFetchDatabases(dbActiveEngine);
    });
}

// ── Quick-navigate to table viewer / query from DB list ────
function dbOpenTableViewer(dbName) {
  tvDb = dbName; tvTable = ''; tvPage = 1;
  dbActiveTab = 'tableviewer';
  renderDbEngine(dbActiveEngine, el('db-engine-panel'));
}

function dbOpenQuery(dbName) {
  curQueryDb = dbName;
  dbActiveTab = 'query';
  renderDbEngine(dbActiveEngine, el('db-engine-panel'));
}

// ── Table Viewer ───────────────────────────────────────────
function dbLoadTableViewer(tc, eng) {
  if (!_dbConnected[eng]) {
    tc.innerHTML = notConnectedMsg(eng); return;
  }
  var ep = eng==='mysql' ? '/api/db/mysql/databases'
         : eng==='postgres' ? '/api/db/pg/databases'
         : '/api/db/databases';

  tc.innerHTML = '<div class="loading" style="font-size:12px">Loading databases...</div>';
  api(ep).then(function(dbs) {
    if (!Array.isArray(dbs)) dbs = [dbs];
    if (!dbs.length) { tc.innerHTML = '<p style="color:#94a3b8;font-size:13px">No databases found.</p>'; return; }
    if (!tvDb) tvDb = dbs[0].name || dbs[0].Name;

    var dbOpts = dbs.map(function(d) {
      var n = d.name || d.Name;
      return '<option value="' + n + '"' + (n===tvDb?' selected':'') + '>' + n + '</option>';
    }).join('');

    tc.innerHTML =
      '<div style="display:grid;grid-template-columns:200px 1fr;gap:14px;align-items:start">' +
        '<div>' +
          '<select id="tv-db" onchange="tvSwitchDb(this.value)" style="width:100%;padding:7px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:12px;margin-bottom:10px">' + dbOpts + '</select>' +
          '<div id="tv-tables" style="max-height:500px;overflow-y:auto"><div class="loading" style="font-size:12px">Loading tables...</div></div>' +
        '</div>' +
        '<div id="tv-main"><div style="text-align:center;padding:60px 0;color:#94a3b8;font-size:13px">Select a table to browse data</div></div>' +
      '</div>';

    tvLoadTables(tvDb);
    if (tvTable) tvLoadRows();
  }).catch(function() { tc.innerHTML = '<div class="err-box">Could not load databases.</div>'; });
}

function tvSwitchDb(db) {
  tvDb = db; tvTable = ''; tvPage = 1;
  tvLoadTables(db);
  el('tv-main').innerHTML = '<div style="text-align:center;padding:60px 0;color:#94a3b8;font-size:13px">Select a table to browse data</div>';
}

function tvLoadTables(db) {
  var cont = el('tv-tables'); if (!cont) return;
  cont.innerHTML = '<div class="loading" style="font-size:12px">Loading...</div>';
  var ep = dbActiveEngine==='mysql' ? '/api/db/mysql/databases/' + encodeURIComponent(db) + '/tables'
         : dbActiveEngine==='postgres' ? '/api/db/pg/databases/' + encodeURIComponent(db) + '/tables'
         : '/api/db/databases/' + encodeURIComponent(db) + '/tables';
  api(ep).then(function(tables) {
    if (!tables || !tables.length) {
      cont.innerHTML = '<p style="font-size:12px;color:#94a3b8;padding:8px 0">No tables found.</p>';
      return;
    }
    cont.innerHTML = tables.map(function(t) {
      var name = t.TABLE_NAME || t.name || t;
      var cnt  = t.RowCount != null ? '<span style="font-size:10px;color:#94a3b8;margin-left:4px">' + t.RowCount + '</span>' : '';
      var isActive = name === tvTable;
      return '<div onclick="tvSelectTable(\'' + name.replace(/'/g,"\\'") + '\')" ' +
        'style="padding:7px 10px;border-radius:6px;cursor:pointer;font-size:13px;display:flex;align-items:center;' +
        'justify-content:space-between;background:' + (isActive?'#eff6ff':'transparent') + ';' +
        'color:' + (isActive?'#1d4ed8':'#475569') + ';margin-bottom:2px">' +
        '<span>▦ ' + name + '</span>' + cnt + '</div>';
    }).join('');
  }).catch(function(e) { cont.innerHTML = '<div class="err-box" style="font-size:12px">' + e.message + '</div>'; });
}

function tvSelectTable(name) {
  tvTable = name; tvPage = 1;
  tvLoadTables(tvDb); // re-render to highlight active
  tvLoadRows();
}

function tvLoadRows() {
  var main = el('tv-main'); if (!main) return;
  main.innerHTML = '<div class="loading">Loading rows...</div>';

  var base = dbActiveEngine==='mysql' ? '/api/db/mysql/databases/'
           : dbActiveEngine==='postgres' ? '/api/db/pg/databases/'
           : '/api/db/databases/';

  var colEp = base + encodeURIComponent(tvDb) + '/tables/' + encodeURIComponent(tvTable) + '/columns';
  var rowEp = base + encodeURIComponent(tvDb) + '/tables/' + encodeURIComponent(tvTable) + '/rows?page=' + tvPage + '&per=50';

  // Fetch columns and rows in parallel
  Promise.all([api(colEp), api(rowEp)]).then(function(res) {
    var cols = res[0], data = res[1];

    if (cols.error) { main.innerHTML = '<div class="err-box">' + cols.error + '</div>'; return; }
    if (data.error) { main.innerHTML = '<div class="err-box">' + data.error + '</div>'; return; }

    var rows  = data.rows || [];
    var total = data.total || 0;
    var pages = data.pages || 1;

    if (!cols.length) { main.innerHTML = '<div class="card"><p style="color:#94a3b8">No columns found.</p></div>'; return; }

    var thead = cols.map(function(c) {
      var pk = c.IS_IDENTITY==1
        ? ' <span style="font-size:9px;background:#dbeafe;color:#1d4ed8;padding:1px 4px;border-radius:3px">PK</span>' : '';
      return '<th style="white-space:nowrap;font-size:12px">' + c.COLUMN_NAME + pk +
        '<br><span style="font-size:10px;font-weight:400;color:#94a3b8">' + c.DATA_TYPE + '</span></th>';
    }).join('');

    var tbody = rows.length ? rows.map(function(row) {
      return '<tr>' + cols.map(function(c) {
        var v = row[c.COLUMN_NAME];
        if (v === null || v === undefined) return '<td style="color:#cbd5e1;font-style:italic;font-size:12px">NULL</td>';
        var s = String(v), d = s.length > 80 ? s.substring(0,80) + '…' : s;
        return '<td style="font-size:12px;white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' +
          s.replace(/"/g,'&quot;').replace(/</g,'&lt;') + '">' + d.replace(/</g,'&lt;') + '</td>';
      }).join('') + '</tr>';
    }).join('') : '<tr><td colspan="' + cols.length + '" style="text-align:center;color:#94a3b8;font-size:13px;padding:20px">No rows found.</td></tr>';

    var pager = pages > 1
      ? '<div style="display:flex;align-items:center;gap:8px;margin-top:12px;font-size:12px;color:#64748b">' +
          '<button class="btn-neutral"' + (tvPage<=1?' disabled':'') + ' onclick="tvChangePage(' + (tvPage-1) + ')">← Prev</button>' +
          '<span>Page ' + tvPage + ' of ' + pages + ' (' + total.toLocaleString() + ' rows)</span>' +
          '<button class="btn-neutral"' + (tvPage>=pages?' disabled':'') + ' onclick="tvChangePage(' + (tvPage+1) + ')">Next →</button>' +
        '</div>'
      : '<p style="font-size:11px;color:#94a3b8;margin-top:8px">' + total.toLocaleString() + ' row(s)</p>';

    main.innerHTML =
      '<div class="card">' +
        '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">' +
          '<h3 style="font-family:monospace;margin:0">' + tvDb + '.<strong>' + tvTable + '</strong></h3>' +
          '<button class="btn-neutral" onclick="tvLoadRows()">↻ Refresh</button>' +
        '</div>' +
        '<div style="overflow-x:auto">' +
          '<table><thead><tr>' + thead + '</tr></thead><tbody>' + tbody + '</tbody></table>' +
        '</div>' +
        pager +
      '</div>';
  }).catch(function(e) {
    main.innerHTML = '<div class="err-box">Error loading data: ' + e.message + '</div>';
  });
}

function tvChangePage(p) { tvPage = p; tvLoadRows(); }

function notConnectedMsg(eng) {
  return '<div style="text-align:center;padding:60px 20px">' +
    '<div style="font-size:36px;margin-bottom:12px">🔌</div>' +
    '<h3 style="margin-bottom:8px;color:#475569">Not Connected</h3>' +
    '<p style="font-size:13px;color:#94a3b8;margin-bottom:16px">Connect to ' + eng + ' in the Databases tab first.</p>' +
    '<button class="btn-primary" onclick="dbSwitchTab(\'databases\')">Go to Databases</button>' +
  '</div>';
}

// ── Query Runner tab ───────────────────────────────────────
function dbLoadQueryRunner(tc, eng) {
  if (!_dbConnected[eng]) { tc.innerHTML = notConnectedMsg(eng); return; }

  var ep = eng==='mysql' ? '/api/db/mysql/databases'
         : eng==='postgres' ? '/api/db/pg/databases'
         : '/api/db/databases';

  api(ep).then(function(dbs) {
    if (!Array.isArray(dbs)) dbs = [dbs];
    var opts = dbs.map(function(d) {
      var n = d.name || d.Name;
      return '<option value="' + n + '"' + (n===curQueryDb?' selected':'') + '>' + n + '</option>';
    }).join('');
    tc.innerHTML =
      '<div style="display:flex;gap:10px;margin-bottom:12px;align-items:center">' +
        '<label style="font-size:13px;color:#64748b;white-space:nowrap">Database:</label>' +
        '<select id="qr-db" style="font-size:13px;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px">' + opts + '</select>' +
      '</div>' +
      '<textarea id="qr-sql" rows="6" placeholder="SELECT * FROM your_table&#10;&#10;-- DROP DATABASE and xp_cmdshell are blocked for safety" ' +
        'style="width:100%;padding:10px;border:1px solid #e2e8f0;border-radius:8px;font-family:monospace;font-size:13px;outline:none;resize:vertical"></textarea>' +
      '<div style="display:flex;justify-content:flex-end;margin-top:10px;gap:8px">' +
        '<button class="btn-neutral" onclick="el(\'qr-sql\').value=\'\'">Clear</button>' +
        '<button class="btn-primary" onclick="runQuery()">▶ Run</button>' +
      '</div>' +
      '<div id="qr-results"></div>';
  }).catch(function() { tc.innerHTML = '<div class="err-box">Cannot load databases.</div>'; });
}

function runQuery() {
  var q  = val('qr-sql').trim(), db = val('qr-db'), r = el('qr-results');
  if (!q) return;
  r.innerHTML = '<div class="loading">Running...</div>';
  var ep = dbActiveEngine==='mysql' ? '/api/db/mysql/query'
         : dbActiveEngine==='postgres' ? '/api/db/pg/query'
         : '/api/db/query';
  api(ep, 'POST', { query:q, database:db }).then(function(res) {
    if (res.error) { r.innerHTML = '<div class="err-box">' + res.error + '</div>'; return; }
    if (res.message) { r.innerHTML = '<div class="qr-msg">' + res.message + '</div>'; return; }
    var rows = res.rows || [];
    if (!rows.length) { r.innerHTML = '<div class="qr-msg">0 rows returned.</div>'; return; }
    var cols = Object.keys(rows[0]);
    r.innerHTML =
      '<div class="qr-results" style="margin-top:12px"><table><thead><tr>' +
      cols.map(function(c){return '<th>'+c+'</th>';}).join('') + '</tr></thead><tbody>' +
      rows.map(function(row){
        return '<tr>' + cols.map(function(c){
          var v=row[c];
          return '<td>'+(v===null?'<span style="color:#94a3b8">NULL</span>':String(v).replace(/</g,'&lt;'))+'</td>';
        }).join('')+'</tr>';
      }).join('') + '</tbody></table></div>' +
      '<p style="font-size:11px;color:#94a3b8;margin-top:8px">' + rows.length + ' row(s) returned</p>';
  }).catch(function(e) { r.innerHTML = '<div class="err-box">' + e.message + '</div>'; });
}

// ── DB Users (SQL Server) ──────────────────────────────────
function loadDbUsers(tc) {
  tc.innerHTML = '<div class="loading">Loading DB users...</div>';
  api('/api/db/users').then(function(list) {
    if (list.error) { tc.innerHTML = '<div class="err-box">' + list.error + '</div>'; return; }
    if (!Array.isArray(list)) list = [list];
    var rows = list.map(function(u) {
      return '<tr>' +
        '<td class="b">'+(u.name||'–')+'</td>' +
        '<td style="font-size:12px;color:#64748b">'+(u.type_desc||'–')+'</td>' +
        '<td><span class="badge '+(u.is_disabled?'stopped':'running')+'">'+(u.is_disabled?'Disabled':'Enabled')+'</span></td>' +
        '<td style="font-size:12px;color:#94a3b8">'+(u.created||'–')+'</td>' +
        '<td><button class="btn-neutral" onclick="openDbPerms(\''+(u.name||'').replace(/'/g,"\\'")+'\')"  >Permissions</button>' +
             '<button class="btn-danger"  onclick="delDbUser(\''+(u.name||'').replace(/'/g,"\\'")+'\')">Delete</button></td>' +
      '</tr>';
    }).join('');
    tc.innerHTML =
      '<div class="search-row"><h3>SQL Server Logins</h3><button class="btn-primary" onclick="openAddDbUser()">+ Create login</button></div>' +
      (rows ? '<table><thead><tr><th>Username</th><th>Type</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>'+rows+'</tbody></table>'
            : '<p style="color:#94a3b8;font-size:13px">No logins found.</p>');
  }).catch(function() { tc.innerHTML = '<div class="err-box">SQL Server not responding.</div>'; });
}

function openAddDbUser() {
  api('/api/db/databases').then(function(dbs) {
    if (!Array.isArray(dbs)) dbs=[dbs];
    el('dbu-db').innerHTML='<option value="none">— none —</option>'+dbs.map(function(d){return '<option>'+(d.name||d.Name)+'</option>';}).join('');
  }).catch(function(){});
  hideErr('dbu-err'); om('m-adddbuser');
}
function submitAddDbUser() {
  hideErr('dbu-err');
  var n=val('dbu-name').trim(), p=val('dbu-pass'), db=val('dbu-db'), r=val('dbu-role');
  if(!n||!p){showErr('dbu-err','Username and password required');return;}
  api('/api/db/users','POST',{username:n,password:p,database:db,role:r}).then(function(res){
    if(res.error){showErr('dbu-err',res.error);return;}
    cm('m-adddbuser'); loadDbUsers(el('db-tab-content'));
  }).catch(function(e){showErr('dbu-err',e.message);});
}
function delDbUser(name){
  if(!confirm('Delete SQL login "'+name+'"?'))return;
  api('/api/db/users/'+name,'DELETE').then(function(){loadDbUsers(el('db-tab-content'));});
}
function openDbPerms(name){
  curDbPermUser=name; el('dbp-uname').textContent=name; hideErr('dbp-err');
  api('/api/db/databases').then(function(dbs){
    if(!Array.isArray(dbs))dbs=[dbs];
    el('dbp-db').innerHTML=dbs.map(function(d){return '<option>'+(d.name||d.Name)+'</option>';}).join('');
  }).catch(function(){});
  om('m-dbperms');
}
function submitDbPerms(){
  hideErr('dbp-err');
  var db=val('dbp-db'), r=val('dbp-role');
  if(!db){showErr('dbp-err','Select a database');return;}
  api('/api/db/users/'+curDbPermUser+'/permissions','POST',{database:db,role:r}).then(function(res){
    if(res.error){showErr('dbp-err',res.error);return;}
    cm('m-dbperms'); alert('Permissions assigned.');
  }).catch(function(e){showErr('dbp-err',e.message);});
}

// ── Engine install ─────────────────────────────────────────
function installEng(key, label) {
  var b = el('instbtn-' + key);
  b.textContent = 'Installing... (5–15 min)'; b.disabled = true;
  api('/api/db/install/' + key, 'POST').then(function(r) {
    if (r.ok) {
      b.textContent = 'Checking...';
      var attempts = 0;
      var poll = setInterval(function() {
        attempts++;
        api('/api/db/status').then(function(st) {
          if (st[key]) { clearInterval(poll); loadDatabase(el('content')); }
          else if (attempts >= 60) { clearInterval(poll); b.textContent = 'Install ' + label; b.disabled = false; }
          else { b.textContent = 'Installing... (' + Math.round(attempts * 15 / 60) + ' min)'; }
        }).catch(function(){});
      }, 15000);
    } else { alert('Failed: ' + (r.error||'Unknown')); b.textContent = 'Install ' + label; b.disabled = false; }
  }).catch(function(e) { alert('Error: ' + e.message); b.textContent = 'Install ' + label; b.disabled = false; });
}



// ── Let's Encrypt helpers (called from SSL page) ───────────
function installWacs(btn) {
  btn.textContent = 'Installing...'; btn.disabled = true;
  api('/api/letsencrypt/install', 'POST').then(function() {
    btn.textContent = 'Installing in background...';
    var attempts = 0;
    var poll = setInterval(function() {
      attempts++;
      api('/api/letsencrypt/status').then(function(s) {
        if (s.installed) { clearInterval(poll); loadSSL(el('content')); }
        else if (attempts > 20) { clearInterval(poll); btn.textContent = 'Install win-acme'; btn.disabled = false; }
      });
    }, 10000);
  }).catch(function(e) { btn.textContent = 'Install win-acme'; btn.disabled = false; alert('Error: ' + e.message); });
}

function requestCert() {
  var domain = val('le-domain').trim(), email = val('le-email').trim(), site = val('le-site').trim();
  var msg = el('le-msg');
  if (!domain || !email) { msg.innerHTML = '<span style="color:#dc2626">Domain and email are required</span>'; return; }
  msg.innerHTML = '<span style="color:#64748b">Sending request... this may take 1-2 minutes</span>';
  api('/api/letsencrypt/request', 'POST', { domain: domain, email: email, siteName: site }).then(function(r) {
    if (r.ok) msg.innerHTML = '<span style="color:#16a34a">✓ ' + r.message + '</span>';
    else      msg.innerHTML = '<span style="color:#dc2626">✗ ' + (r.error || 'Failed') + '</span>';
  }).catch(function(e) { msg.innerHTML = '<span style="color:#dc2626">Error: ' + e.message + '</span>'; });
}

function renewAllCerts() {
  api('/api/letsencrypt/renew', 'POST').then(function(r) {
    alert(r.message || (r.ok ? 'Renewal started in background.' : r.error));
  }).catch(function(e) { alert('Error: ' + e.message); });
}


// ── 3. FILE MANAGER ────────────────────────────────────────
var _fileSelection = new Set();

function loadFiles(c, dir) {
  _fileSelection = new Set();
  curFilePath = dir || 'C:\\';

  c.innerHTML =
    '<div class="card">' +
      '<div class="search-row"><h3>File Manager</h3>' +
        '<div style="display:flex;gap:8px">' +
          '<button class="btn-neutral" disabled>↻ Refresh</button>' +
          '<button class="btn-neutral" disabled>+ New folder</button>' +
          '<button class="btn-primary" disabled>⬆ Upload</button>' +
        '</div>' +
      '</div>' +
      '<div class="breadcrumb"><div class="sk-line" style="width:180px;height:12px"></div></div>' +
      '<div>' +
        [1,2,3,4,5,6,7].map(function(i) {
          var isFolder = i <= 3;
          return '<div style="display:flex;align-items:center;padding:9px 10px;gap:10px">' +
            '<div class="sk-line" style="width:14px;height:14px;border-radius:3px;flex-shrink:0"></div>' +
            '<div class="sk-line" style="width:14px;height:14px;border-radius:2px;flex-shrink:0"></div>' +
            '<div class="sk-line" style="flex:1;height:12px;width:' + (isFolder?'30':'55') + '%"></div>' +
            '<div class="sk-line" style="width:80px;height:10px"></div>' +
            '<div class="sk-line" style="width:50px;height:10px"></div>' +
          '</div>';
        }).join('') +
      '</div>' +
    '</div>';

  api('/api/files/list?path=' + encodeURIComponent(curFilePath)).then(function(data) {
    if (data.error) { c.innerHTML = '<div class="err-box">' + data.error + '</div>'; return; }

    var parts = curFilePath.replace(/\\/g, '/').split('/').filter(Boolean);
    var bc    = '<span onclick="loadFiles(el(\'content\'),\'C:\\\\\')" >C:\\</span>';
    var built = 'C:\\';
    parts.slice(1).forEach(function(p) {
      built += p + '\\';
      var snap = built;
      bc += '<span class="bsep">›</span><span onclick="loadFiles(el(\'content\'),\'' + snap.replace(/\\/g,'\\\\') + '\')">' + p + '</span>';
    });

    var fileRows = data.items.map(function(f) {
      var fpath    = (curFilePath.endsWith('\\') ? curFilePath : curFilePath + '\\') + f.name;
      var safePath = fpath.replace(/\\/g,'\\\\');
      var mod      = f.modified ? new Date(f.modified).toLocaleString() : '';
      var isZip    = /\.zip$/i.test(f.name);

      var actions = '';
      if (isZip)    actions += '<button class="btn-neutral" style="font-size:11px;padding:3px 8px" onclick="event.stopPropagation();unzipFile(\'' + safePath + '\')" title="Extract here">📂</button>';
      if (!f.isDir) actions += '<button class="btn-neutral" onclick="event.stopPropagation();downloadFile(\'' + safePath + '\')" title="Download">⬇</button>';
      actions += '<button class="btn-neutral" style="font-size:11px;padding:3px 8px" onclick="event.stopPropagation();toggleHideFile(\'' + safePath + '\',' + f.hidden + ')" title="' + (f.hidden ? 'Unhide' : 'Hide') + '">' + (f.hidden ? '👁' : '🙈') + '</button>';
      actions += '<button class="btn-neutral" style="font-size:11px;padding:3px 8px" onclick="event.stopPropagation();toggleLockFile(\'' + safePath + '\',' + f.readonly + ')" title="' + (f.readonly ? 'Unlock' : 'Lock') + '">' + (f.readonly ? '🔓' : '🔒') + '</button>';
      actions += '<button class="btn-danger"  onclick="event.stopPropagation();delFile(\'' + safePath + '\')" title="Delete">✕</button>';

      var rowStyle = f.hidden ? 'opacity:0.55;' : '';
      return '<div class="file-row' + (f.isDir?' folder':'') + '" style="' + rowStyle + '"' +
        (f.isDir ? ' onclick="loadFiles(el(\'content\'),\'' + safePath + '\')"' : '') + '>' +
        '<label class="fcheck" onclick="event.stopPropagation()" style="display:flex;align-items:center;padding-right:6px;cursor:pointer">' +
          '<input type="checkbox" data-path="' + fpath.replace(/"/g,'&quot;') + '" onchange="onFileCheck(this)">' +
        '</label>' +
        '<span class="ficon">' + (f.isDir ? '📁' : getFileIcon(f.name)) + '</span>' +
        '<span class="fname">' + f.name + '</span>' +
        (f.hidden   ? '<span style="font-size:10px;color:#94a3b8;margin-left:6px;font-family:monospace">hidden</span>' : '') +
        (f.readonly ? '<span style="font-size:11px;margin-left:4px" title="Read-only">🔒</span>' : '') +
        '<span class="fmod" style="font-size:11px;color:#94a3b8;min-width:130px;text-align:right">' + mod + '</span>' +
        '<span class="fsize">' + (f.isDir ? '' : formatBytes(f.size)) + '</span>' +
        '<div class="faction">' + actions + '</div>' +
      '</div>';
    }).join('') || '<div style="padding:20px;color:#94a3b8;font-size:13px">This folder is empty</div>';

    c.innerHTML =
      '<div class="card">' +
        '<div class="search-row"><h3>File Manager</h3>' +
          '<div style="display:flex;gap:8px;align-items:center">' +
            '<button class="btn-neutral" onclick="loadFiles(el(\'content\'),curFilePath)">↻ Refresh</button>' +
            '<button class="btn-neutral" onclick="om(\'m-mkdir\')">+ New folder</button>' +
            '<button class="btn-primary" onclick="om(\'m-upload\')">⬆ Upload</button>' +
          '</div>' +
        '</div>' +
        '<div id="file-sel-bar" style="display:none;padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;margin-bottom:10px;align-items:center;gap:10px;flex-wrap:wrap">' +
          '<span id="file-sel-count" style="font-size:12px;color:#1d4ed8;font-weight:500;margin-right:4px">0 selected</span>' +
          '<button class="btn-neutral" style="font-size:11px" onclick="zipSelected()">🗜 Zip</button>' +
          '<button class="btn-danger"  style="font-size:11px" onclick="deleteSelected()">🗑 Delete</button>' +
          '<button class="btn-neutral" style="font-size:11px" onclick="clearFileSelection()">✕ Clear</button>' +
        '</div>' +
        '<div class="breadcrumb">' + bc + '</div>' +
        '<div>' + fileRows + '</div>' +
      '</div>';
  }).catch(function() { c.innerHTML = '<div class="err-box">Could not load files.</div>'; });
}

function onFileCheck(chk) {
  var p = chk.dataset.path;
  if (chk.checked) _fileSelection.add(p);
  else _fileSelection.delete(p);
  var bar = el('file-sel-bar');
  var cnt = el('file-sel-count');
  if (bar) bar.style.display = _fileSelection.size > 0 ? 'flex' : 'none';
  if (cnt) cnt.textContent  = _fileSelection.size + ' selected';
}

function clearFileSelection() {
  _fileSelection.clear();
  document.querySelectorAll('.file-row input[type=checkbox]').forEach(function(c) { c.checked = false; });
  var bar = el('file-sel-bar'); if (bar) bar.style.display = 'none';
}

function zipSelected() {
  if (!_fileSelection.size) return;
  var name = prompt('Zip filename:', 'archive.zip');
  if (!name) return;
  if (!name.toLowerCase().endsWith('.zip')) name += '.zip';
  var dest = (curFilePath.endsWith('\\') ? curFilePath : curFilePath + '\\') + name;
  api('/api/files/zip', 'POST', { files: Array.from(_fileSelection), dest: dest }).then(function(r) {
    if (r.error) { alert('Zip failed: ' + r.error); return; }
    clearFileSelection();
    loadFiles(el('content'), curFilePath);
  });
}

function deleteSelected() {
  if (!_fileSelection.size) return;
  if (!confirm('Delete ' + _fileSelection.size + ' selected item(s)? This cannot be undone.')) return;
  api('/api/files/delete-multi', 'DELETE', { paths: Array.from(_fileSelection) }).then(function(r) {
    clearFileSelection();
    loadFiles(el('content'), curFilePath);
  });
}

function unzipFile(filePath) {
  var cleanPath = filePath.replace(/\\\\/g, '\\');
  var dest = cleanPath.substring(0, cleanPath.lastIndexOf('\\'));
  if (!confirm('Extract "' + cleanPath.split('\\').pop() + '" into this folder?')) return;
  api('/api/files/unzip', 'POST', { file: cleanPath, dest: dest }).then(function(r) {
    if (r.error) { alert('Unzip failed: ' + r.error); return; }
    loadFiles(el('content'), curFilePath);
  });
}

function toggleHideFile(filePath, isHidden) {
  var cleanPath = filePath.replace(/\\\\/g, '\\');
  api('/api/files/hide', 'POST', { path: cleanPath, hidden: !isHidden }).then(function(r) {
    if (r.error) { alert('Failed: ' + r.error); return; }
    loadFiles(el('content'), curFilePath);
  });
}

function toggleLockFile(filePath, isLocked) {
  var cleanPath = filePath.replace(/\\\\/g, '\\');
  api('/api/files/lock', 'POST', { path: cleanPath, locked: !isLocked }).then(function(r) {
    if (r.error) { alert('Failed: ' + r.error); return; }
    loadFiles(el('content'), curFilePath);
  });
}

function getFileIcon(name) {
  var ext = (name.split('.').pop() || '').toLowerCase();
  var icons = { js:'📜', ts:'📜', json:'📋', html:'🌐', css:'🎨', xml:'📋',
    zip:'🗜', rar:'🗜', '7z':'🗜', gz:'🗜', tar:'🗜',
    jpg:'🖼', jpeg:'🖼', png:'🖼', gif:'🖼', svg:'🖼', ico:'🖼', webp:'🖼',
    mp4:'🎬', avi:'🎬', mov:'🎬', mp3:'🎵', wav:'🎵',
    pdf:'📕', doc:'📝', docx:'📝', xls:'📊', xlsx:'📊', csv:'📊',
    txt:'📄', log:'📄', md:'📄', bat:'⚙', ps1:'⚙', sh:'⚙', exe:'⚙', dll:'⚙' };
  return icons[ext] || '📄';
}

function downloadFile(filePath) {
  var cleanPath = filePath.replace(/\\\\/g, '\\');
  var url = API + '/api/files/download?path=' + encodeURIComponent(cleanPath);
  fetch(url, { headers: { Authorization: 'Bearer ' + TOKEN } })
    .then(function(r) {
      if (!r.ok) return r.json().then(function(d) { alert('Download failed: ' + (d.error || 'Unknown error')); });
      var cd = r.headers.get('content-disposition') || '';
      var match = cd.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
      var filename = match ? match[1].replace(/['"]/g, '') : cleanPath.split('\\').pop();
      return r.blob().then(function(blob) {
        var burl = URL.createObjectURL(blob);
        var link = document.createElement('a');
        link.href = burl; link.download = filename;
        document.body.appendChild(link); link.click(); document.body.removeChild(link);
        setTimeout(function() { URL.revokeObjectURL(burl); }, 2000);
      });
    })
    .catch(function(e) { alert('Error: ' + e.message); });
}

function delFile(filePath) {
  var cleanPath = filePath.replace(/\\\\/g, '\\');
  if (!confirm('Delete "' + cleanPath + '"?')) return;
  api('/api/files/delete?path=' + encodeURIComponent(cleanPath), 'DELETE').then(function() {
    loadFiles(el('content'), curFilePath);
  });
}

function submitMkdir() {
  var name = val('mkdir-name').trim(); if (!name) return;
  var full = (curFilePath.endsWith('\\') ? curFilePath : curFilePath + '\\') + name;
  api('/api/files/mkdir', 'POST', { path: full }).then(function() { cm('m-mkdir'); loadFiles(el('content'), curFilePath); });
}

function submitUpload() {
  var input = el('upload-files');
  if (!input || !input.files.length) { showErr('upload-err', 'Select at least one file'); return; }
  var bar = el('upload-progress'), barFill = el('upload-bar-fill'), statusMsg = el('upload-msg');
  bar.style.display = 'block'; statusMsg.textContent = 'Uploading...'; statusMsg.style.color = '#64748b';
  hideErr('upload-err');
  var fd = new FormData();
  Array.from(input.files).forEach(function(f) { fd.append('files', f); });
  var xhr = new XMLHttpRequest();
  xhr.open('POST', API + '/api/files/upload?path=' + encodeURIComponent(curFilePath));
  xhr.setRequestHeader('Authorization', 'Bearer ' + TOKEN);
  xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) { barFill.style.width = Math.round(e.loaded / e.total * 100) + '%'; }
  };
  xhr.onload = function() {
    bar.style.display = 'none'; barFill.style.width = '0%';
    try {
      var resp = JSON.parse(xhr.responseText);
      if (resp.ok) {
        statusMsg.textContent = '✓ Uploaded ' + resp.uploaded.join(', ');
        statusMsg.style.color = '#16a34a';
        input.value = '';
        setTimeout(function() { cm('m-upload'); loadFiles(el('content'), curFilePath); }, 900);
      } else {
        showErr('upload-err', resp.error || 'Upload failed'); statusMsg.textContent = '';
      }
    } catch(e2) { showErr('upload-err', 'Server error'); statusMsg.textContent = ''; }
  };
  xhr.onerror = function() { bar.style.display = 'none'; showErr('upload-err', 'Network error'); statusMsg.textContent = ''; };
  xhr.send(fd);
}


// ── 5. SMTP ────────────────────────────────────────────────
function loadSMTP(c) {
  c.innerHTML =
    '<div class="card" style="margin-bottom:16px">' +
      '<div class="sk-line" style="width:160px;height:16px;margin-bottom:20px"></div>' +
      '<div class="form-grid-2">' +
        [1,2,3,4,5,6].map(function() {
          return '<div>' +
            '<div class="sk-line" style="width:80px;height:11px;margin-bottom:6px"></div>' +
            '<div class="sk-line" style="width:100%;height:34px;border-radius:6px"></div>' +
          '</div>';
        }).join('') +
      '</div>' +
      '<div style="margin-top:20px;display:flex;gap:8px">' +
        '<div class="sk-line" style="width:110px;height:34px;border-radius:6px"></div>' +
      '</div>' +
    '</div>' +
    '<div class="card">' +
      '<div class="sk-line" style="width:140px;height:16px;margin-bottom:14px"></div>' +
      '<div style="display:flex;gap:8px">' +
        '<div class="sk-line" style="flex:1;height:34px;border-radius:6px"></div>' +
        '<div class="sk-line" style="width:130px;height:34px;border-radius:6px"></div>' +
      '</div>' +
    '</div>';

  var cached = cacheGet('smtp');
  if (cached) { renderSMTPForm(c, cached); return; }

  api('/api/smtp').then(function(cfg) {
    cacheSet('smtp', cfg);
    renderSMTPForm(c, cfg);
  }).catch(function() { c.innerHTML = '<div class="err-box">Cannot reach backend.</div>'; });
}

function renderSMTPForm(c, cfg) {
  c.innerHTML =
    '<div class="card">' +
      '<h3>SMTP Configuration</h3>' +
      '<div class="form-grid-2">' +
        '<div class="form-field"><label>SMTP Host</label><input id="sm-host" value="' + (cfg.host||'') + '" placeholder="smtp.example.com" /></div>' +
        '<div class="form-field"><label>Port</label><input id="sm-port" type="number" value="' + (cfg.port||587) + '" /></div>' +
        '<div class="form-field"><label>Username</label><input id="sm-user" value="' + (cfg.user||'') + '" placeholder="user@example.com" /></div>' +
        '<div class="form-field"><label>Password</label><input id="sm-pass" type="password" placeholder="' + (cfg.hasPass?'•••••• (saved)':'Enter password') + '" /></div>' +
        '<div class="form-field"><label>From address</label><input id="sm-from" value="' + (cfg.from||'') + '" placeholder="noreply@example.com" /></div>' +
        '<div class="toggle-row"><input type="checkbox" id="sm-secure" ' + (cfg.secure?'checked':'') + ' /><label for="sm-secure">Use SSL/TLS (port 465)</label></div>' +
      '</div>' +
      '<div style="margin-top:20px;display:flex;align-items:center;gap:12px">' +
        '<button class="btn-primary" onclick="saveSMTP()">Save settings</button>' +
        '<span id="sm-saved" style="font-size:12px;color:#16a34a;display:none">✓ Saved</span>' +
      '</div>' +
    '</div>' +
    '<div class="card">' +
      '<h3>Send test email</h3>' +
      '<div style="display:flex;gap:8px">' +
        '<input id="sm-test-to" placeholder="recipient@example.com" style="flex:1;padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;outline:none" />' +
        '<button class="btn-primary" onclick="testSMTP()">Send test email</button>' +
      '</div>' +
      '<div id="sm-test-result" style="font-size:13px;margin-top:10px"></div>' +
    '</div>';
}

// ── Users & Permissions ───────────────────────────────────
var selectedUserId = null;
var _usersTab = 'users'; // 'users' | 'traffic'

function switchUsersTab(tab) {
  _usersTab = tab;
  document.querySelectorAll('.utab').forEach(function(t) {
    t.classList.toggle('active',
      (tab === 'users'   && t.dataset.tab === 'users') ||
      (tab === 'traffic' && t.dataset.tab === 'traffic'));
  });
  var c = el('content');
  if (tab === 'users')   renderUsersTab(c);
  if (tab === 'traffic') renderTrafficTab(c);
}
var ALL_PERMS_LABELS = {
  dashboard:'Dashboard', processes:'Task Manager', logs:'Event Logs',
  sites:'IIS Sites', domains:'Domain Manager', ssl:'SSL Certs',
  services:'Services', database:'Databases', files:'File Manager',
  smtp:'SMTP', scheduler:'Task Scheduler', security:'Security Check',
  users:'Users', settings:'Settings'
};

var _userList = []; // module-level cache for onclick lookups

function loadUsers(c) {
  if (CU.role !== 'admin') {
    c.innerHTML = '<div class="placeholder"><div class="ph-icon">🔒</div><h3>Admin only</h3><p>You need admin access to manage users.</p></div>';
    return;
  }

  c.innerHTML =
    '<div style="display:flex;gap:8px;margin-bottom:16px">' +
      '<div class="sk-line" style="width:90px;height:32px;border-radius:6px"></div>' +
      '<div class="sk-line" style="width:140px;height:32px;border-radius:6px"></div>' +
    '</div>' +
    '<div style="display:grid;grid-template-columns:260px 1fr;gap:16px">' +
      '<div class="card" style="padding:12px">' +
        '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">' +
          '<div class="sk-line" style="width:50px;height:14px"></div>' +
          '<div class="sk-line" style="width:60px;height:28px;border-radius:6px"></div>' +
        '</div>' +
        [1,2,3,4].map(function() {
          return '<div style="display:flex;align-items:center;gap:10px;padding:10px 0">' +
            '<div class="sk-line" style="width:32px;height:32px;border-radius:50%;flex-shrink:0"></div>' +
            '<div style="flex:1">' +
              '<div class="sk-line" style="width:80%;height:12px;margin-bottom:5px"></div>' +
              '<div class="sk-line" style="width:50%;height:10px"></div>' +
            '</div>' +
          '</div>';
        }).join('') +
      '</div>' +
      '<div class="card">' +
        '<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">' +
          '<div class="sk-line" style="width:40px;height:40px;border-radius:50%;flex-shrink:0"></div>' +
          '<div>' +
            '<div class="sk-line" style="width:120px;height:14px;margin-bottom:6px"></div>' +
            '<div class="sk-line" style="width:80px;height:11px"></div>' +
          '</div>' +
        '</div>' +
        [1,2,3,4,5,6].map(function() {
          return '<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f8fafc">' +
            '<div class="sk-line" style="width:120px;height:12px"></div>' +
            '<div class="sk-line" style="width:50px;height:22px;border-radius:4px"></div>' +
          '</div>';
        }).join('') +
      '</div>' +
    '</div>';

  var tabBar =
    '<div class="tabs" style="margin-bottom:16px">' +
      '<div class="utab tab' + (_usersTab==='users' ? ' active' : '') + '" data-tab="users" onclick="switchUsersTab(\x27users\x27)">&#128100; Users &amp; Permissions</div>' +
      '<div class="utab tab' + (_usersTab==='traffic' ? ' active' : '') + '" data-tab="traffic" onclick="switchUsersTab(\x27traffic\x27)">&#128225; Traffic &amp; IP Manager</div>' +
    '</div>' +
    '<div id="users-tab-body"></div>';

  setTimeout(function() {
    c.innerHTML = tabBar;
    if (_usersTab === 'users')   renderUsersTab(c);
    if (_usersTab === 'traffic') renderTrafficTab(c);
  }, 0);
}

function renderUsersTab(c) {
  var body = el('users-tab-body'); if (!body) return;

  // Show cache instantly if available
  var cached = cacheGet('users');
  if (cached) {
    _userList = cached;
    renderUsersList(body, cached);
    return;
  }

  body.innerHTML = '<div class="loading">Loading users...</div>';
  api('/api/users').then(function(list) {
    _userList = list;
    cacheSet('users', list);
    renderUsersList(body, list);
  }).catch(function() { body.innerHTML = '<div class="err-box">Could not load users.</div>'; });
}

function renderUsersList(body, list) {
  var userListHTML = list.map(function(u, idx) {
    var isSel = u.id === selectedUserId;
    return '<div class="user-list-item" onclick="selectUserByIndex(' + idx + ',this)" ' +
      'style="display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:8px;cursor:pointer;border:1px solid ' + (isSel ? '#bfdbfe' : 'transparent') + ';background:' + (isSel ? '#eff6ff' : 'transparent') + '">' +
      '<div style="width:32px;height:32px;border-radius:50%;background:' + (u.role==='admin'?'#1e3a5f':'#1e3a2a') + ';display:flex;align-items:center;justify-content:center;color:' + (u.role==='admin'?'#38bdf8':'#34d399') + ';font-size:13px;font-weight:500;flex-shrink:0">' + u.username[0].toUpperCase() + '</div>' +
      '<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:500;color:#0f172a">' + u.username + '</div>' +
      '<div style="font-size:11px;color:#94a3b8">' + (u.permissions||[]).length + ' permissions</div></div>' +
      '<span class="badge ' + (u.role==='admin'?'admin':'viewer') + '" style="font-size:10px">' + u.role + '</span>' +
    '</div>';
  }).join('');

  if (!selectedUserId && list[0]) selectedUserId = list[0].id;
  var selectedUser = list.find(function(u) { return u.id === selectedUserId; }) || list[0];

  body.innerHTML =
    '<div style="display:grid;grid-template-columns:260px 1fr;gap:16px;align-items:start">' +
      '<div><div class="card" style="padding:12px">' +
        '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">' +
          '<span style="font-size:13px;font-weight:500">Users</span>' +
          '<div style="display:flex;gap:6px">' +
            '<button class="btn-neutral" style="font-size:11px;padding:4px 8px" onclick="cacheClear(\'users\');renderUsersTab(el(\'content\'))">&#8635;</button>' +
            '<button class="btn-primary" style="font-size:11px;padding:4px 10px" onclick="openAddUser()">+ Add</button>' +
          '</div>' +
        '</div>' +
        '<div id="user-list">' + userListHTML + '</div>' +
      '</div></div>' +
      '<div id="user-detail-panel">' + renderUserDetail(selectedUser) + '</div>' +
    '</div>';
}

function renderTrafficTab(c) {
  var body = el('users-tab-body'); if (!body) return;

  // Show cache instantly if available
  var cached = cacheGet('traffic');
  if (cached) {
    _trafficData = cached.traffic || [];
    _blockedIPs  = cached.blocked || [];
    renderTrafficInto(body);
    return;
  }

  body.innerHTML =
    '<div style="display:flex;align-items:center;gap:10px;padding:20px 0">' +
      '<div class="loading"></div>' +
      '<span style="font-size:13px;color:#64748b">Parsing IIS logs and looking up geo-data...</span>' +
    '</div>';

  api('/api/traffic').then(function(resp) {
    _trafficData = resp.traffic || [];
    _blockedIPs  = resp.blocked || [];
    cacheSet('traffic', resp);
    renderTrafficInto(body);
  }).catch(function() {
    body.innerHTML = '<div class="err-box">Could not load traffic data. Ensure IIS is installed and access logs exist.</div>';
  });
}

function renderTrafficInto(body) {
  var blockedSet = new Set(_blockedIPs.map(function(b) {
    return b.ip.split(',').map(function(s) { return s.trim(); });
  }).flat());

  var sorted = (_trafficFilter
    ? _trafficData.filter(function(r) {
        return r.ip.includes(_trafficFilter) ||
               (r.country || '').toLowerCase().includes(_trafficFilter.toLowerCase()) ||
               (r.city || '').toLowerCase().includes(_trafficFilter.toLowerCase());
      })
    : _trafficData.slice()).sort(function(a, b) {
      return _trafficSort === 'hits' ? b.hits - a.hits : b.bytes - a.bytes;
    });

  var trafficRows = sorted.map(function(r) {
    var isBlocked = r.blocked || blockedSet.has(r.ip);
    var flag = r.cc ? countryFlag(r.cc) + ' ' : '';
    var geo  = r.country ? (flag + r.country + (r.city ? ', ' + r.city : '')) : '<span style="color:#cbd5e1">&#8211;</span>';
    var safeIp = r.ip.replace(/'/g, '');
    return '<tr style="' + (isBlocked ? 'background:#fef2f2' : '') + '">' +
      '<td class="b" style="font-family:monospace;font-size:12px">' + r.ip + '</td>' +
      '<td style="font-size:12px">' + geo + '</td>' +
      '<td style="font-size:12px;text-align:right;font-weight:500">' + (r.hits || 0).toLocaleString() + '</td>' +
      '<td style="font-size:12px;text-align:right;color:#64748b">' + formatBytes(r.bytes || 0) + '</td>' +
      '<td style="font-size:12px;color:#94a3b8;font-family:monospace">' + (r.topMethod || '&#8211;') + '</td>' +
      '<td style="font-size:11px;color:#94a3b8;white-space:nowrap">' + (r.lastSeen || '&#8211;') + '</td>' +
      '<td>' +
        (isBlocked
          ? '<span class="badge error" style="font-size:10px;margin-right:4px">Blocked</span>' +
            '<button class="btn-neutral" style="font-size:11px" onclick="unblockIPAndRefreshTab(\x27' + safeIp + '\x27)">Unblock</button>'
          : '<button class="btn-stop" style="font-size:11px" onclick="openBlockIPTab(\x27' + safeIp + '\x27)">Block</button>') +
      '</td>' +
    '</tr>';
  }).join('');

  var blockedRows = _blockedIPs.map(function(b) {
    var safeIp = b.ip.split(',')[0].trim().replace(/'/g, '');
    return '<tr>' +
      '<td class="b" style="font-family:monospace;font-size:12px">' + b.ip + '</td>' +
      '<td style="font-size:12px;color:#64748b">' + b.name + '</td>' +
      '<td><span class="badge ' + (b.enabled ? 'error' : 'stopped') + '">' + (b.enabled ? 'Active' : 'Disabled') + '</span></td>' +
      '<td><button class="btn-neutral" style="font-size:11px" onclick="unblockIPAndRefreshTab(\x27' + safeIp + '\x27)">Remove</button></td>' +
    '</tr>';
  }).join('');

  body.innerHTML =
    '<div class="card" style="margin-bottom:16px">' +
      '<div class="search-row">' +
        '<h3>&#128225; Incoming Traffic</h3>' +
        '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
          '<input placeholder="Filter IP / country..." ' +
            'oninput="_trafficFilter=this.value;renderTrafficInto(el(\x27users-tab-body\x27))" ' +
            'style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px;width:150px" />' +
          '<select onchange="_trafficSort=this.value;renderTrafficInto(el(\x27users-tab-body\x27))" ' +
            'style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px">' +
            '<option value="hits"' + (_trafficSort === 'hits' ? ' selected' : '') + '>Sort: Hits</option>' +
            '<option value="bytes"' + (_trafficSort === 'bytes' ? ' selected' : '') + '>Sort: Bytes</option>' +
          '</select>' +
          '<button class="btn-neutral" onclick="cacheClear(\'traffic\');renderTrafficTab(el(\'content\'))">&#8635; Refresh</button>'
          '<button class="btn-danger" onclick="openBlockIPTab(\x27\x27)">+ Block IP</button>' +
        '</div>' +
      '</div>' +
      (sorted.length === 0
        ? '<div style="text-align:center;padding:30px 0;color:#94a3b8;font-size:13px">No IIS traffic data found.</div>'
        : '<div style="overflow-x:auto"><table>' +
            '<thead><tr><th>IP Address</th><th>Country</th>' +
            '<th style="text-align:right">Hits</th><th style="text-align:right">Bytes</th>' +
            '<th>Method</th><th>Last Seen</th><th>Action</th></tr></thead>' +
            '<tbody>' + trafficRows + '</tbody></table></div>' +
            '<p style="font-size:11px;color:#94a3b8;margin-top:8px">' + sorted.length + ' unique IPs &#8212; geo via ip-api.com</p>') +
    '</div>' +
    '<div class="card">' +
      '<div class="search-row"><h3>&#128683; Blocked IPs</h3>' +
        '<button class="btn-primary" style="font-size:11px" onclick="openBlockIPTab(\x27\x27)">+ Block IP</button>' +
      '</div>' +
      (_blockedIPs.length === 0
        ? '<p style="color:#94a3b8;font-size:13px">No IPs currently blocked by ServerPanel rules.</p>'
        : '<table><thead><tr><th>IP / Range</th><th>Rule Name</th><th>Status</th><th>Action</th></tr></thead>' +
          '<tbody>' + blockedRows + '</tbody></table>') +
    '</div>';
}

// Wrappers that refresh the tab view instead of the full page
function openBlockIPTab(ip) {
  el('bip-ip').value     = ip || '';
  el('bip-reason').value = '';
  hideErr('bip-err');
  // Override submit to refresh tab
  el('bip-ip').dataset.fromTab = '1';
  om('m-blockip');
}
function unblockIPAndRefreshTab(ip) {
  if (!confirm('Remove firewall block for ' + ip + '?')) return;
  api('/api/traffic/block', 'DELETE', { ip: ip }).then(function(r) {
    if (r.error) { alert(r.error); return; }
    renderTrafficTab(el('content'));
  });
}

function renderUserDetail(u) {
  if (!u) return '<div class="card"><p style="color:#94a3b8;font-size:13px">Select a user.</p></div>';
  var permsHTML = ALL_PERMS.map(function(p) {
    var has = (u.permissions||[]).includes(p);
    var label = ALL_PERMS_LABELS[p] || p;
    return '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f8fafc">' +
      '<span style="font-size:13px;color:#475569">' + label + '</span>' +
      '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin:0">' +
        '<input type="checkbox" class="perm-toggle" data-perm="' + p + '" ' + (has?'checked':'') + ' ' + (u.role==='admin'?'disabled':'') + ' style="width:auto;cursor:pointer" onchange="inlinePermChange(' + u.id + ',this)" />' +
        '<span style="font-size:11px;color:' + (has?'#16a34a':'#94a3b8') + '">' + (has?'Allowed':'Denied') + '</span>' +
      '</label>' +
    '</div>';
  }).join('');
  return '<div class="card">' +
    '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">' +
      '<div style="display:flex;align-items:center;gap:12px">' +
        '<div style="width:40px;height:40px;border-radius:50%;background:' + (u.role==='admin'?'#1e3a5f':'#1e3a2a') + ';display:flex;align-items:center;justify-content:center;color:' + (u.role==='admin'?'#38bdf8':'#34d399') + ';font-size:16px;font-weight:500">' + u.username[0].toUpperCase() + '</div>' +
        '<div><div style="font-size:15px;font-weight:500">' + u.username + '</div>' +
        '<div style="font-size:12px;color:#94a3b8">Created ' + formatDate(new Date(u.createdAt)) + '</div></div>' +
      '</div>' +
      '<div style="display:flex;gap:8px">' +
        '<span class="badge ' + (u.role==='admin'?'admin':'viewer') + '">' + u.role + '</span>' +
        (u.id !== 1 ? '<button class="btn-danger" onclick="delUser(' + u.id + ',\'' + u.username + '\')">Delete</button>' : '') +
      '</div>' +
    '</div>' +
    (u.role==='admin' ? '<div style="padding:10px;background:#eff6ff;border-radius:6px;font-size:13px;color:#1d4ed8;margin-bottom:12px">Admin has full access. Toggles disabled.</div>' : '') +
    '<div style="margin-bottom:6px;font-size:12px;font-weight:500;color:#475569">Page Permissions</div>' +
    permsHTML +
    '<div id="perm-save-msg" style="margin-top:10px;font-size:12px;color:#16a34a;display:none">✓ Permissions saved</div>' +
  '</div>';
}

function selectUserByIndex(idx, elem) {
  var u = _userList[idx];
  if (!u) return;
  selectUser(u.id, u, elem);
}

function selectUser(id, u, elem) {
  selectedUserId = id;
  document.querySelectorAll('.user-list-item').forEach(function(item) {
    item.style.background = 'transparent'; item.style.borderColor = 'transparent';
  });
  elem.style.background = '#eff6ff'; elem.style.borderColor = '#bfdbfe';
  el('user-detail-panel').innerHTML = renderUserDetail(u);
}

function inlinePermChange(userId, checkbox) {
  var perms = [];
  document.querySelectorAll('.perm-toggle').forEach(function(cb) { if (cb.checked) perms.push(cb.dataset.perm); });
  api('/api/users/' + userId + '/permissions', 'PUT', { permissions: perms }).then(function(r) {
    if (r.ok) {
      var span = checkbox.nextElementSibling;
      if (span) { span.textContent = checkbox.checked ? 'Allowed' : 'Denied'; span.style.color = checkbox.checked ? '#16a34a' : '#94a3b8'; }
      var msg = el('perm-save-msg');
      if (msg) { msg.style.display = 'block'; setTimeout(function(){ msg.style.display='none'; }, 2000); }
    }
  }).catch(function() {});
}

function openAddUser() {
  buildPermGrid('nu-perm-grid', []);
  el('nu-name').value = ''; el('nu-pass').value = '';
  el('nu-role').value = 'admin';
  el('nu-perm-wrap').style.display = 'none';
  hideErr('nu-err'); om('m-adduser');
}
function togglePermGrid() {
  el('nu-perm-wrap').style.display = val('nu-role') === 'viewer' ? 'block' : 'none';
}
function buildPermGrid(gridId, selected) {
  el(gridId).innerHTML = ALL_PERMS.map(function(p) {
    var label = ALL_PERMS_LABELS[p] || p;
    return '<div class="perm-item">' +
      '<input type="checkbox" id="perm-' + gridId + '-' + p + '" value="' + p + '"' + (selected.includes(p) ? ' checked' : '') + ' />' +
      '<label for="perm-' + gridId + '-' + p + '">' + label + '</label>' +
    '</div>';
  }).join('');
}
function getCheckedPerms(gridId) {
  return ALL_PERMS.filter(function(p) { var cb = el('perm-' + gridId + '-' + p); return cb && cb.checked; });
}
function submitAddUser() {
  hideErr('nu-err');
  var n = val('nu-name').trim(), p = val('nu-pass'), r = val('nu-role');
  if (!n || !p) { showErr('nu-err', 'Username and password are required'); return; }
  var perms = r === 'admin' ? ALL_PERMS : getCheckedPerms('nu-perm-grid');
  api('/api/users', 'POST', { username: n, password: p, role: r, permissions: perms }).then(function(res) {
    if (res.error) { showErr('nu-err', res.error); return; }
    cm('m-adduser'); 
    cacheClear('users');
    loadUsers(el('content'));
  }).catch(function(e) { showErr('nu-err', e.message); });
}
function openPermsModal(id, name, perms) {
  curPermUserId = id;
  el('pm-uname').textContent = name;
  buildPermGrid('pm-grid', perms || []);
  om('m-perms');
}
function submitPerms() {
  var perms = getCheckedPerms('pm-grid');
  api('/api/users/' + curPermUserId + '/permissions', 'PUT', { permissions: perms }).then(function(r) {
    if (r.ok) { cm('m-perms'); loadUsers(el('content')); }
    else alert('Failed: ' + (r.error || 'Unknown error'));
  }).catch(function(e) { alert('Error: ' + e.message); });
}
function delUser(id, name) {
  if (!confirm('Delete user "' + name + '"? This cannot be undone.')) return;
  api('/api/users/' + id, 'DELETE').then(function() { selectedUserId = null; 
  cacheClear('users'); 
  loadUsers(el('content')); });
}

// ── Settings ──────────────────────────────────────────────
function loadSettings(c) {
  if (CU.role !== 'admin') {
    c.innerHTML = '<div class="placeholder"><div class="ph-icon">🔒</div><h3>Admin only</h3></div>';
    return;
  }

// ← ADD skeleton immediately
  c.innerHTML =
    '<div style="max-width:640px">' +
      '<div class="card" style="margin-bottom:16px">' +
        '<div class="sk-line" style="width:80px;height:16px;margin-bottom:20px"></div>' +
        '<div class="form-grid-2">' +
          [1,2,3].map(function() {
            return '<div>' +
              '<div class="sk-line" style="width:90px;height:11px;margin-bottom:6px"></div>' +
              '<div class="sk-line" style="width:100%;height:34px;border-radius:6px"></div>' +
            '</div>';
          }).join('') +
        '</div>' +
      '</div>' +
      '<div class="card" style="margin-bottom:16px">' +
        '<div class="sk-line" style="width:140px;height:16px;margin-bottom:16px"></div>' +
        [1,2].map(function() {
          return '<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">' +
            '<div class="sk-line" style="width:18px;height:18px;border-radius:3px;flex-shrink:0"></div>' +
            '<div class="sk-line" style="width:200px;height:13px"></div>' +
          '</div>';
        }).join('') +
      '</div>' +
      '<div class="sk-line" style="width:120px;height:34px;border-radius:6px"></div>' +
    '</div>';

  var cached = cacheGet('settings');
  if (cached) { renderSettingsForm(c, cached); return; }

  c.innerHTML = '<div class="loading">Loading settings...</div>';
  api('/api/settings').then(function(cfg) {
    cacheSet('settings', cfg);
    renderSettingsForm(c, cfg);
  }).catch(function() { c.innerHTML = '<div class="err-box">Cannot reach backend.</div>'; });
}

function renderSettingsForm(c, cfg) {
  c.innerHTML =
    '<div style="max-width:640px">' +
      '<div class="card">' +
        '<h3 style="margin-bottom:16px">General</h3>' +
        '<div class="form-grid-2">' +
          '<div class="form-field"><label>Panel name</label><input id="st-name" value="' + (cfg.panelName||'ServerPanel') + '" /></div>' +
          '<div class="form-field"><label>Timezone</label>' +
            '<select id="st-tz">' +
              ['UTC','Asia/Kolkata','America/New_York','America/Chicago','America/Los_Angeles','Europe/London','Europe/Berlin','Asia/Tokyo','Australia/Sydney'].map(function(tz) {
                return '<option value="' + tz + '"' + (cfg.timezone===tz?' selected':'') + '>' + tz + '</option>';
              }).join('') +
            '</select></div>' +
          '<div class="form-field"><label>Session timeout (hours)</label><input id="st-session" type="number" min="1" max="72" value="' + (cfg.sessionTimeout||8) + '" /></div>' +
        '</div>' +
      '</div>' +
      '<div class="card">' +
        '<h3 style="margin-bottom:4px">Crash Monitoring</h3>' +
        '<p style="font-size:12px;color:#94a3b8;margin-bottom:14px">Records unhandled exceptions to the Crash Logs tab.</p>' +
        '<div class="toggle-row" style="margin-bottom:14px"><input type="checkbox" id="st-crash" ' + (cfg.crashMonitor?'checked':'') + ' /><label for="st-crash">Enable crash monitoring</label></div>' +
        '<div class="toggle-row" style="margin-bottom:12px"><input type="checkbox" id="st-email" ' + (cfg.crashEmailEnabled?'checked':'') + ' onchange="toggleCrashEmail()" /><label for="st-email">Send email alert on crash</label></div>' +
        '<div id="st-email-fields" style="' + (cfg.crashEmailEnabled?'':'display:none') + ';padding:12px;background:#fef2f2;border-radius:8px;border:1px solid #fca5a5">' +
          '<div class="form-field" style="margin-bottom:10px"><label>Alert recipient email</label><input id="st-emailto" value="' + (cfg.crashEmailTo||'') + '" placeholder="you@example.com" /></div>' +
          '<div class="form-field"><label>Email subject</label><input id="st-emailsubj" value="' + (cfg.crashEmailSubject||'ServerPanel — System Crash Detected') + '" /></div>' +
          '<p style="font-size:11px;color:#94a3b8;margin-top:8px">Uses SMTP settings from Configuration → SMTP.</p>' +
        '</div>' +
      '</div>' +
      '<div style="display:flex;align-items:center;gap:12px">' +
        '<button class="btn-primary" onclick="saveSettings()">Save settings</button>' +
        '<span id="st-saved" style="font-size:12px;color:#16a34a;display:none">✓ Saved</span>' +
      '</div>' +
    '</div>';
}

function toggleCrashEmail() {
  el('st-email-fields').style.display = el('st-email').checked ? 'block' : 'none';
}

function saveSettings() {
  var data = {
    panelName: val('st-name'), timezone: val('st-tz'),
    sessionTimeout: parseInt(val('st-session'))||8,
    crashMonitor: el('st-crash').checked,
    crashEmailEnabled: el('st-email').checked,
    crashEmailTo: val('st-emailto')||'',
    crashEmailSubject: val('st-emailsubj')||''
  };
  api('/api/settings', 'POST', data).then(function(r) {
    if (r.ok) {
      cacheClear('settings');
      var s = el('st-saved');
      s.style.display = 'inline';
      setTimeout(function(){ s.style.display = 'none'; }, 3000);
    } else {
      alert('Save failed: ' + (r.error||'Unknown'));
    }
  }).catch(function(e) { alert('Error: ' + e.message); });
}

function toggleCrashEmail() {
  el('st-email-fields').style.display = el('st-email').checked ? 'block' : 'none';
}

function saveSettings() {
  var data = {
    panelName: val('st-name'), timezone: val('st-tz'),
    sessionTimeout: parseInt(val('st-session'))||8,
    crashMonitor: el('st-crash').checked,
    crashEmailEnabled: el('st-email').checked,
    crashEmailTo: val('st-emailto')||'',
    crashEmailSubject: val('st-emailsubj')||''
  };
  api('/api/settings', 'POST', data).then(function(r) {
    if (r.ok) {
      cacheClear('settings');
      var s = el('st-saved');
      s.style.display = 'inline';
      setTimeout(function(){ s.style.display = 'none'; }, 3000);
    } else {
      alert('Save failed: ' + (r.error||'Unknown'));
    }
  }).catch(function(e) { alert('Error: ' + e.message); });
}

// ── 7. SECURITY CHECK ──────────────────────────────────────
function loadSecurity(c) {
  c.innerHTML =
    '<div class="card" style="margin-bottom:16px">' +
      '<div class="search-row">' +
        '<h3>🛡 Security Check</h3>' +
        '<button class="btn-neutral" disabled>↻ Rescan</button>' +
      '</div>' +
      '<div style="display:flex;align-items:center;gap:20px;padding:16px;background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0;margin-bottom:16px">' +
        '<div style="text-align:center;min-width:80px">' +
          '<div class="sk-line" style="width:60px;height:36px;margin:0 auto 8px;border-radius:6px"></div>' +
          '<div class="sk-line" style="width:80px;height:11px;margin:0 auto"></div>' +
        '</div>' +
        '<div style="flex:1">' +
          '<div class="sk-line" style="width:100%;height:10px;margin-bottom:8px;border-radius:4px"></div>' +
          '<div class="sk-line" style="width:50%;height:11px"></div>' +
        '</div>' +
      '</div>' +
      [1,2,3,4,5,6].map(function() {
        return '<div style="display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:8px;background:#f8fafc;margin-bottom:6px">' +
          '<div class="sk-line" style="width:18px;height:18px;border-radius:50%;flex-shrink:0"></div>' +
          '<div style="flex:1">' +
            '<div class="sk-line" style="width:45%;height:13px;margin-bottom:5px"></div>' +
            '<div class="sk-line" style="width:70%;height:11px"></div>' +
          '</div>' +
        '</div>';
      }).join('') +
    '</div>' +
    '<div class="card">' +
      '<div class="sk-line" style="width:100px;height:16px;margin-bottom:14px"></div>' +
      skeletonTable(['Port','Protocol','Process','Action'], 4) +
    '</div>';

  var cached = cacheGet('security');
  if (cached) { renderSecurityPage(c, cached.scan, cached.ports); return; }

  Promise.all([
    api('/api/security/scan'),
    api('/api/security/open-ports')
  ]).then(function(res) {
    cacheSet('security', { scan: res[0], ports: res[1] });
    renderSecurityPage(c, res[0], res[1]);
  }).catch(function() {
    c.innerHTML = '<div class="err-box">Security scan failed. Run backend as Administrator.</div>';
  });
}

function renderSecurityPage(c, results, ports) {
  // Build security score + checks HTML
  var byCategory = {};
  results.forEach(function(r) { if (!byCategory[r.category]) byCategory[r.category]=[]; byCategory[r.category].push(r); });
  var score = results.filter(function(r){ return r.status==='pass'; }).length;
  var pct = Math.round(score/results.length*100);
  var scoreColor = pct>=80?'#16a34a':pct>=50?'#d97706':'#dc2626';

  var scanHtml = '<div style="display:flex;align-items:center;gap:20px;padding:16px;background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0;margin-bottom:16px">' +
    '<div style="text-align:center;min-width:80px"><div style="font-size:36px;font-weight:700;color:' + scoreColor + '">' + pct + '%</div><div style="font-size:11px;color:#94a3b8">Security Score</div></div>' +
    '<div style="flex:1"><div style="background:#e5e7eb;border-radius:4px;height:10px"><div style="width:' + pct + '%;height:100%;background:' + scoreColor + ';border-radius:4px"></div></div>' +
    '<div style="font-size:12px;color:#64748b;margin-top:6px">' + score + ' of ' + results.length + ' checks passed</div></div></div>';

  Object.keys(byCategory).forEach(function(cat) {
    scanHtml += '<div style="margin-bottom:16px"><div style="font-size:11px;font-weight:600;color:#475569;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">' + cat + '</div>';
    byCategory[cat].forEach(function(r) {
      var icon  = r.status==='pass'?'✓':r.status==='fail'?'✗':'⚠';
      var color = r.status==='pass'?'#16a34a':r.status==='fail'?'#dc2626':r.status==='warn'?'#d97706':'#3b82f6';
      var bg    = r.status==='pass'?'#f0fdf4':r.status==='fail'?'#fef2f2':r.status==='warn'?'#fffbeb':'#eff6ff';
      scanHtml += '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:8px;background:' + bg + ';margin-bottom:6px">' +
        '<div style="display:flex;align-items:center;gap:10px">' +
          '<span style="font-size:16px;color:' + color + '">' + icon + '</span>' +
          '<div><div style="font-size:13px;font-weight:500">' + r.title + '</div>' +
          '<div style="font-size:12px;color:#64748b">' + r.detail + '</div></div>' +
        '</div>' +
        (r.fix ? '<button class="btn-primary" style="font-size:11px;padding:4px 10px" onclick="applySecFix(\'' + r.fix + '\',this)">Fix</button>' : '') +
      '</div>';
    });
    scanHtml += '</div>';
  });

  // Build ports HTML
  var portsHtml = !ports.length
    ? '<p style="color:#94a3b8;font-size:13px">No listening ports found.</p>'
    : '<table><thead><tr><th>Port</th><th>Protocol</th><th>Process</th><th>Action</th></tr></thead><tbody>' +
      ports.map(function(p) {
        return '<tr><td class="b">' + p.LocalPort + '</td><td class="m">TCP</td>' +
          '<td style="font-size:12px;color:#64748b">' + (p.Process||'-') + '</td>' +
          '<td><button class="btn-stop" style="font-size:11px" onclick="quickBlock(' + p.LocalPort + ')">Block</button></td></tr>';
      }).join('') + '</tbody></table>';

  c.innerHTML =
    '<div class="card" style="margin-bottom:16px">' +
      '<div class="search-row"><h3>🛡 Security Check</h3>' +
        '<button class="btn-neutral" onclick="cacheClear(\'security\');loadSecurity(el(\'content\'))">↻ Rescan</button>' +
      '</div>' +
      scanHtml +
    '</div>' +
    '<div class="card">' +
      '<h3>Open Ports</h3>' +
      portsHtml +
    '</div>';
}

function fetchSecurityScan() {
  api('/api/security/scan').then(function(results) {
    var byCategory = {};
    results.forEach(function(r) { if (!byCategory[r.category]) byCategory[r.category]=[]; byCategory[r.category].push(r); });
    var score = results.filter(function(r){return r.status==='pass';}).length;
    var pct = Math.round(score/results.length*100);
    var scoreColor = pct>=80?'#16a34a':pct>=50?'#d97706':'#dc2626';
    var html = '<div style="display:flex;align-items:center;gap:20px;padding:16px;background:#f8fafc;border-radius:10px;border:1px solid #e2e8f0;margin-bottom:16px">' +
      '<div style="text-align:center;min-width:80px"><div style="font-size:36px;font-weight:700;color:' + scoreColor + '">' + pct + '%</div><div style="font-size:11px;color:#94a3b8">Security Score</div></div>' +
      '<div style="flex:1"><div style="background:#e5e7eb;border-radius:4px;height:10px"><div style="width:' + pct + '%;height:100%;background:' + scoreColor + ';border-radius:4px"></div></div>' +
      '<div style="font-size:12px;color:#64748b;margin-top:6px">' + score + ' of ' + results.length + ' checks passed</div></div></div>';
    Object.keys(byCategory).forEach(function(cat) {
      html += '<div style="margin-bottom:16px"><div style="font-size:11px;font-weight:600;color:#475569;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">' + cat + '</div>';
      byCategory[cat].forEach(function(r) {
        var icon = r.status==='pass'?'✓':r.status==='fail'?'✗':'⚠';
        var color = r.status==='pass'?'#16a34a':r.status==='fail'?'#dc2626':r.status==='warn'?'#d97706':'#3b82f6';
        var bg = r.status==='pass'?'#f0fdf4':r.status==='fail'?'#fef2f2':r.status==='warn'?'#fffbeb':'#eff6ff';
        html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:8px;background:' + bg + ';margin-bottom:6px">' +
          '<div style="display:flex;align-items:center;gap:10px">' +
            '<span style="font-size:16px;color:' + color + '">' + icon + '</span>' +
            '<div><div style="font-size:13px;font-weight:500">' + r.title + '</div>' +
            '<div style="font-size:12px;color:#64748b">' + r.detail + '</div></div>' +
          '</div>' +
          (r.fix ? '<button class="btn-primary" style="font-size:11px;padding:4px 10px" onclick="applySecFix(\'' + r.fix + '\',this)">Fix</button>' : '') +
        '</div>';
      });
      html += '</div>';
    });
    el('sec-results').innerHTML = html;
  }).catch(function() { el('sec-results').innerHTML = '<div class="err-box">Scan failed. Run backend as Administrator.</div>'; });
}
function fetchOpenPorts() {
  api('/api/security/open-ports').then(function(list) {
    if (!list.length) { el('ports-list').innerHTML = '<p style="color:#94a3b8;font-size:13px">No listening ports found.</p>'; return; }
    var rows = list.map(function(p) {
      return '<tr><td class="b">' + p.LocalPort + '</td><td class="m">TCP</td><td style="font-size:12px;color:#64748b">' + (p.Process||'-') + '</td>' +
        '<td><button class="btn-stop" style="font-size:11px" onclick="quickBlock(' + p.LocalPort + ')">Block</button></td></tr>';
    }).join('');
    el('ports-list').innerHTML = '<table><thead><tr><th>Port</th><th>Protocol</th><th>Process</th><th>Action</th></tr></thead><tbody>' + rows + '</tbody></table>';
  }).catch(function() { el('ports-list').innerHTML = '<div class="err-box">Could not load ports.</div>'; });
}
function applySecFix(action, btn) {
  if (!confirm('Apply fix: ' + action + '?')) return;
  btn.textContent = 'Applying...'; btn.disabled = true;
  api('/api/security/fix', 'POST', { action: action }).then(function(r) {
    if (r.ok) { alert('Fix applied.'); loadSecurity(el('content')); }
    else { alert('Failed: ' + (r.error||'Unknown')); btn.textContent = 'Fix'; btn.disabled = false; }
  }).catch(function(e) { alert('Error: ' + e.message); btn.textContent = 'Fix'; btn.disabled = false; });
}
function fwOpenPort() {
  var port = val('fp-port'), proto = val('fp-proto'), name = val('fp-name');
  if (!port) { el('fw-msg').innerHTML = '<span style="color:#dc2626">Enter a port number</span>'; return; }
  api('/api/security/open-port', 'POST', { port: parseInt(port), protocol: proto, name: name }).then(function(r) {
    el('fw-msg').innerHTML = r.ok ? '<span style="color:#16a34a">✓ Port ' + port + ' opened</span>' : '<span style="color:#dc2626">' + r.error + '</span>';
    fetchOpenPorts();
  });
}
function fwBlockPort() {
  var port = val('fp-port'), proto = val('fp-proto');
  if (!port) { el('fw-msg').innerHTML = '<span style="color:#dc2626">Enter a port number</span>'; return; }
  if (!confirm('Block ' + proto + ' port ' + port + '?')) return;
  api('/api/security/block-port', 'POST', { port: parseInt(port), protocol: proto }).then(function(r) {
    el('fw-msg').innerHTML = r.ok ? '<span style="color:#16a34a">✓ Port ' + port + ' blocked</span>' : '<span style="color:#dc2626">' + r.error + '</span>';
    fetchOpenPorts();
  });
}
function quickBlock(port) {
  if (!confirm('Block TCP port ' + port + '?')) return;
  api('/api/security/block-port', 'POST', { port: port, protocol: 'TCP' }).then(function(r) {
    if (r.ok) fetchOpenPorts(); else alert('Failed: ' + r.error);
  });
}

// ── 6. TASK SCHEDULER ──────────────────────────────────────
function loadScheduler(c) {
  c.innerHTML = skeletonCard('Scheduled Tasks',
    ['Name','State','Last Run','Next Run','Last Result','Actions'], 5,
    '<button class="btn-neutral" disabled>↻ Refresh</button>' +
    '<button class="btn-primary" disabled>+ New task</button>');

  var cached = cacheGet('scheduler');
  if (cached) { renderSchedulerList(c, cached); return; }

  api('/api/scheduler').then(function(list) {
    cacheSet('scheduler', list);
    renderSchedulerList(c, list);
  }).catch(function() { c.innerHTML = '<div class="err-box">Could not load scheduled tasks.</div>'; });
}

function renderSchedulerList(c, list) {
  var rows = list.map(function(t) {
    var stColor  = t.State==='Running'||t.State==='Ready' ? 'running' : 'stopped';
    var lastRes  = t.LastResult===0
      ? '<span style="color:#16a34a">Success</span>'
      : t.LastResult!==null
        ? '<span style="color:#dc2626">Error (' + t.LastResult + ')</span>'
        : '–';
    var safeName = t.Name.replace(/'/g, "\\'");
    return '<tr>' +
      '<td class="b">' + t.Name + '</td>' +
      '<td><span class="badge ' + stColor + '">' + t.State + '</span></td>' +
      '<td style="font-size:12px;color:#64748b">' + (t.LastRun||'–') + '</td>' +
      '<td style="font-size:12px;color:#64748b">' + (t.NextRun||'–') + '</td>' +
      '<td>' + lastRes + '</td>' +
      '<td style="white-space:nowrap">' +
        '<button class="btn-start" onclick="taskAction(\'' + safeName + '\',\'run\')">▶ Run</button>' +
        (t.State==='Ready'
          ? '<button class="btn-neutral" onclick="taskAction(\'' + safeName + '\',\'disable\')">Disable</button>'
          : '<button class="btn-neutral" onclick="taskAction(\'' + safeName + '\',\'enable\')">Enable</button>') +
        '<button class="btn-danger" onclick="taskAction(\'' + safeName + '\',\'delete\')">Delete</button>' +
      '</td>' +
    '</tr>';
  }).join('');

  c.innerHTML =
    '<div class="card">' +
      '<div class="search-row"><h3>Scheduled Tasks</h3>' +
        '<div style="display:flex;gap:8px">' +
          '<button class="btn-neutral" onclick="cacheClear(\'scheduler\');loadScheduler(el(\'content\'))">↻ Refresh</button>' +
          '<button class="btn-primary" onclick="om(\'m-addtask\')">+ New task</button>' +
        '</div>' +
      '</div>' +
      (rows
        ? '<table><thead><tr><th>Name</th><th>State</th><th>Last Run</th><th>Next Run</th><th>Last Result</th><th>Actions</th></tr></thead><tbody>' + rows + '</tbody></table>'
        : '<p style="color:#94a3b8;font-size:13px">No user-created tasks found.</p>') +
    '</div>';
}

function taskAction(name, action) {
  if (action==='delete' && !confirm('Delete task "' + name + '"?')) return;
  var method = action==='delete' ? 'DELETE' : 'POST';
  var suffix = action==='delete' ? '' : '/' + action;
  api('/api/scheduler/' + encodeURIComponent(name) + suffix, method).then(function(r) {
    if (r.error) alert(r.error);
    cacheClear('scheduler');
    loadScheduler(el('content'));
  });
}
function submitAddTask() {
  hideErr('at-err');
  var name = val('at-name').trim(), execute = val('at-execute').trim();
  if (!name || !execute) { showErr('at-err', 'Name and execute path are required'); return; }
  var body = {
    name: name, description: val('at-desc'), execute: execute,
    arguments: val('at-args'), workingDir: val('at-wdir'),
    triggerType: val('at-trigger'), triggerTime: val('at-time'),
    triggerDay: val('at-day'), triggerInterval: val('at-interval')
  };
  api('/api/scheduler', 'POST', body).then(function(r) {
    if (r.error) { showErr('at-err', r.error); return; }
    cm('m-addtask'); 
    cacheClear('scheduler'); 
    loadScheduler(el('content'));
  }).catch(function(e) { showErr('at-err', e.message); });
}
function toggleTriggerFields() {
  var t = val('at-trigger');
  el('at-time-row').style.display   = ['once','daily','weekly','hourly'].includes(t) ? 'block' : 'none';
  el('at-day-row').style.display    = t==='weekly'  ? 'block' : 'none';
  el('at-int-row').style.display    = t==='hourly'  ? 'block' : 'none';
}



// ── Audit Log ─────────────────────────────────────────────
function loadAuditLog(c) {
  if (CU.role !== 'admin') {
    c.innerHTML = '<div class="placeholder"><div class="ph-icon">🔒</div><h3>Admin only</h3><p>Audit log is only visible to administrators.</p></div>';
    return;
  }
  c.innerHTML = '<div class="loading">Loading audit log...</div>';

  api('/api/audit/users').then(function(users) {
    var userOpts = '<option value="">All users</option>' + users.map(function(u) {
      return '<option value="' + u + '">' + u + '</option>';
    }).join('');

    c.innerHTML =
      '<div class="card">' +
        '<div class="search-row">' +
          '<h3>🗒 Audit Log</h3>' +
          '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
            '<select id="audit-user" onchange="fetchAuditLog()" style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px">' + userOpts + '</select>' +
            '<input id="audit-search" placeholder="Filter by action…" oninput="filterAuditRows()" style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px;width:160px" />' +
            '<button class="btn-neutral" onclick="fetchAuditLog()">↻ Refresh</button>' +
            '<button class="btn-danger" onclick="clearAuditLog()">🗑 Clear</button>' +
          '</div>' +
        '</div>' +
        '<div id="audit-body"><div class="loading">Loading...</div></div>' +
      '</div>';

    fetchAuditLog();
  }).catch(function() { c.innerHTML = '<div class="err-box">Could not load audit log.</div>'; });
}

var _auditRows = [];
function fetchAuditLog() {
  var body = el('audit-body'); if (!body) return;
  body.innerHTML = '<div class="loading">Loading...</div>';
  var user = (el('audit-user') || {}).value || '';
  api('/api/audit?limit=500' + (user ? '&user=' + encodeURIComponent(user) : '')).then(function(list) {
    _auditRows = list;
    renderAuditRows(list);
  }).catch(function() { body.innerHTML = '<div class="err-box">Could not load entries.</div>'; });
}

function filterAuditRows() {
  var q = (el('audit-search') || {}).value || '';
  if (!q) { renderAuditRows(_auditRows); return; }
  var lq = q.toLowerCase();
  renderAuditRows(_auditRows.filter(function(e) {
    return (e.action || '').toLowerCase().includes(lq) || (e.detail || '').toLowerCase().includes(lq);
  }));
}

var ACTION_COLORS = {
  'auth.login': 'info', 'auth.login.fail': 'error',
  'users.create': 'info', 'users.delete': 'error', 'users.password': 'warn', 'users.permissions': 'warn',
  'files.upload': 'info', 'files.download': 'info', 'files.delete': 'error', 'files.mkdir': 'info',
  'sites.create': 'info', 'sites.start': 'running', 'sites.stop': 'stopped',
  'sites.restart': 'warn', 'sites.edit': 'warn', 'sites.backup': 'info',
  'audit.clear': 'error'
};

function renderAuditRows(list) {
  var body = el('audit-body'); if (!body) return;
  if (!list.length) {
    body.innerHTML = '<div style="text-align:center;padding:40px 0"><div style="font-size:32px;margin-bottom:12px">📋</div><p style="color:#64748b;font-size:13px">No audit entries found.</p></div>';
    return;
  }
  var rows = list.map(function(e) {
    var bc = ACTION_COLORS[e.action] || 'info';
    return '<tr>' +
      '<td style="font-size:11px;white-space:nowrap;color:#64748b">' + (e.time || '') + '</td>' +
      '<td class="b" style="font-size:12px">' + (e.user || '–') + '</td>' +
      '<td style="font-size:11px;color:#94a3b8;font-family:monospace">' + (e.ip || '–') + '</td>' +
      '<td><span class="badge ' + bc + '" style="font-family:monospace;font-size:11px">' + (e.action || '') + '</span></td>' +
      '<td style="font-size:12px;color:#475569;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + (e.detail || '').replace(/"/g, '&quot;') + '">' + (e.detail || '') + '</td>' +
    '</tr>';
  }).join('');
  body.innerHTML = '<table><thead><tr><th>Time</th><th>User</th><th>IP</th><th>Action</th><th>Detail</th></tr></thead><tbody>' + rows + '</tbody></table>' +
    '<p style="font-size:11px;color:#94a3b8;margin-top:10px;text-align:right">' + list.length + ' entries shown</p>';
}

function clearAuditLog() {
  if (!confirm('Clear the entire audit log? This cannot be undone.')) return;
  api('/api/audit', 'DELETE').then(function(r) {
    if (r.ok) fetchAuditLog();
    else alert('Failed: ' + (r.error || 'Unknown'));
  });
}

// ── Change Password (self-service) ────────────────────────
function openChangePassword() {
  el('cp-current').value = '';
  el('cp-new').value = '';
  el('cp-confirm').value = '';
  hideErr('cp-err');
  el('cp-ok').style.display = 'none';
  om('m-change-password');
}

function submitChangePassword() {
  hideErr('cp-err');
  el('cp-ok').style.display = 'none';
  var current = el('cp-current').value;
  var newPass  = el('cp-new').value;
  var confirm  = el('cp-confirm').value;

  if (!newPass || newPass.length < 6) { showErr('cp-err', 'New password must be at least 6 characters'); return; }
  if (newPass !== confirm)            { showErr('cp-err', 'Passwords do not match'); return; }
  if (CU.role !== 'admin' && !current) { showErr('cp-err', 'Current password is required'); return; }

  var userId = CU.id;
  if (!userId) { showErr('cp-err', 'Cannot determine your user ID — please log out and back in'); return; }

  api('/api/users/' + userId + '/password', 'POST', { currentPassword: current, password: newPass })
    .then(function(r) {
      if (r.ok) {
        el('cp-ok').style.display = 'block';
        el('cp-current').value = ''; el('cp-new').value = ''; el('cp-confirm').value = '';
        setTimeout(function() { cm('m-change-password'); }, 1500);
      } else {
        showErr('cp-err', r.error || 'Failed to change password');
      }
    })
    .catch(function(e) { showErr('cp-err', e.message); });
}



// ── App Pool Manager ───────────────────────────────────────
var _curEditPool = null;

function loadAppPools(c) {
  c.innerHTML = skeletonCard('IIS Application Pools',
    ['Name','State','.NET Version','Pipeline','Workers','Actions'], 5,
    '<button class="btn-neutral" disabled>&#8635; Refresh</button>' +
    '<button class="btn-primary" disabled>+ New pool</button>');
  var cached = cacheGet('apppools');
  if (cached) {
    renderAppPools(c, cached);
    return;
  }
  api('/api/apppools').then(function(list) {
    if (!Array.isArray(list)) list = [list];
    cacheSet('apppools', list);
    renderAppPools(c, list);
  }).catch(function() { c.innerHTML = '<div class="err-box">Could not load app pools. Ensure IIS is installed.</div>'; });
}

function renderAppPools(c, list) {
  var rows = list.map(function(p) {
    var isRunning = p.state === 'Started';
    var stateClass = isRunning ? 'running' : 'stopped';
    var dotNet = p.dotNetVersion || 'No Managed Code';
    var safeName = (p.name || '').replace(/'/g, "\\'");
    return '<tr>' +
      '<td class="b">' + (p.name || '') + '</td>' +
      '<td><span class="badge ' + stateClass + '"><span class="bdot"></span>' + (p.state || '') + '</span></td>' +
      '<td style="font-family:monospace;font-size:12px">' + dotNet + '</td>' +
      '<td style="font-size:12px;color:#64748b">' + (p.pipelineMode || '') + '</td>' +
      '<td style="font-size:12px;color:#64748b">' + (p.workerProcesses || 0) + ' worker(s)</td>' +
      '<td style="white-space:nowrap">' +
        (isRunning
          ? '<button class="btn-stop" onclick="poolAction(\'' + safeName + '\',\'stop\')">Stop</button>'
          : '<button class="btn-start" onclick="poolAction(\'' + safeName + '\',\'start\')">Start</button>') +
        '<button class="btn-neutral" onclick="poolAction(\'' + safeName + '\',\'recycle\')">&#8635; Recycle</button>' +
        '<button class="btn-neutral" onclick="openEditPool(\'' + safeName + '\',' +
          '\'' + (p.dotNetVersion||'v4.0').replace(/'/g,"\\'") + '\',' +
          '\'' + (p.pipelineMode||'Integrated') + '\',' +
          (p.enable32Bit ? 'true' : 'false') + ',' +
          (p.queueLength || 1000) + ')">Edit</button>' +
        '<button class="btn-danger" onclick="poolAction(\'' + safeName + '\',\'delete\')">Delete</button>' +
      '</td>' +
    '</tr>';
  }).join('');

  c.innerHTML =
    '<div class="card">' +
      '<div class="search-row">' +
        '<h3>IIS Application Pools</h3>' +
        '<div style="display:flex;gap:8px">' +
          '<button class="btn-neutral" onclick="cacheClear(\'apppools\');loadAppPools(el(\'content\'))">&#8635; Refresh</button>' +
          '<button class="btn-primary" onclick="om(\'m-addpool\')">+ New pool</button>' +
        '</div>' +
      '</div>' +
      '<table>' +
        '<thead><tr>' +
          '<th>Name</th><th>State</th><th>.NET Version</th>' +
          '<th>Pipeline</th><th>Workers</th><th>Actions</th>' +
        '</tr></thead>' +
        '<tbody>' + rows + '</tbody>' +
      '</table>' +
    '</div>';
}

function poolAction(name, action) {
  if (action === 'delete' && !confirm('Delete app pool "' + name + '"?\nThis cannot be undone and may break sites using it.')) return;
  var method = action === 'delete' ? 'DELETE' : 'POST';
  var url = '/api/apppools/' + encodeURIComponent(name) + (action === 'delete' ? '' : '/' + action);
  api(url, method).then(function(r) {
    if (r.error) alert(r.error);
    cacheClear('apppools');        // ← ADD THIS
    loadAppPools(el('content'));
  });
}

function openEditPool(name, dotNet, pipeline, is32, queue) {
  _curEditPool = name;
  el('ep-name-label').textContent = name;
  el('ep-dotnet').value   = dotNet || 'v4.0';
  el('ep-pipeline').value = pipeline || 'Integrated';
  el('ep-32bit').checked  = !!is32;
  el('ep-queue').value    = queue || 1000;
  hideErr('ep-err');
  om('m-editpool');
}

function submitEditPool() {
  hideErr('ep-err');
  if (!_curEditPool) return;
  var body = {
    dotNetVersion: val('ep-dotnet'),
    pipelineMode:  val('ep-pipeline'),
    enable32Bit:   el('ep-32bit').checked,
    queueLength:   parseInt(val('ep-queue')) || 1000
  };
  api('/api/apppools/' + encodeURIComponent(_curEditPool), 'PUT', body).then(function(r) {
    if (r.error) { showErr('ep-err', r.error); return; }
    cm('m-editpool');
    loadAppPools(el('content'));
  }).catch(function(e) { showErr('ep-err', e.message); });
}

function submitAddPool() {
  hideErr('ap-err');
  var name = val('ap-name').trim();
  if (!name) { showErr('ap-err', 'Pool name is required'); return; }
  var body = {
    name:         name,
    dotNetVersion: val('ap-dotnet'),
    pipelineMode:  val('ap-pipeline'),
    enable32Bit:   el('ap-32bit').checked
  };
  api('/api/apppools', 'POST', body).then(function(r) {
    if (r.error) { showErr('ap-err', r.error); return; }
    cm('m-addpool');
    loadAppPools(el('content'));
  }).catch(function(e) { showErr('ap-err', e.message); });
}

// ── Traffic & IP Manager ───────────────────────────────────
var _trafficData = [];
var _blockedIPs  = [];
var _trafficSort = 'hits';
var _trafficFilter = '';

// ── 8. TRAFFIC & IPs ───────────────────────────────────────
function loadTraffic(c) {
  c.innerHTML =
    skeletonCard('📡 Incoming Traffic',
      ['IP Address','Country','Hits','Bytes','Method','Last Seen','Action'], 6,
      '<button class="btn-neutral" disabled>↻ Refresh</button>' +
      '<button class="btn-danger"  disabled>+ Block IP</button>') +
    skeletonCard('🚫 Blocked IPs',
      ['IP / Range','Rule Name','Status','Action'], 2,
      '<button class="btn-primary" disabled>+ Block IP</button>');

  api('/api/traffic').then(function(resp) {
    _trafficData = resp.traffic || [];
    _blockedIPs  = resp.blocked || [];
    renderTraffic(c);
  }).catch(function() {
    c.innerHTML = '<div class="err-box">Could not load traffic data. Ensure IIS is installed and logs exist.</div>';
  });
}

function renderTraffic(c) {
  var blockedSet = new Set(_blockedIPs.map(function(b) {
    return b.ip.split(',').map(function(s) { return s.trim(); });
  }).flat());

  var filtered = _trafficFilter
    ? _trafficData.filter(function(r) {
        return r.ip.includes(_trafficFilter) ||
               (r.country || '').toLowerCase().includes(_trafficFilter.toLowerCase()) ||
               (r.city || '').toLowerCase().includes(_trafficFilter.toLowerCase());
      })
    : _trafficData;

  var sorted = filtered.slice().sort(function(a, b) {
    return _trafficSort === 'hits' ? b.hits - a.hits : b.bytes - a.bytes;
  });

  var trafficRows = sorted.map(function(r) {
    var isBlocked = r.blocked || blockedSet.has(r.ip);
    var flag = r.cc ? '<span title="' + (r.country || '') + '" style="margin-right:4px">' + countryFlag(r.cc) + '</span>' : '';
    var geo  = r.country ? (flag + r.country + (r.city ? ', ' + r.city : '')) : '<span style="color:#cbd5e1">–</span>';
    return '<tr style="' + (isBlocked ? 'background:#fef2f2' : '') + '">' +
      '<td class="b" style="font-family:monospace;font-size:12px">' + r.ip + '</td>' +
      '<td>' + geo + '</td>' +
      '<td style="font-size:12px;text-align:right;font-weight:500">' + r.hits.toLocaleString() + '</td>' +
      '<td style="font-size:12px;text-align:right;color:#64748b">' + formatBytes(r.bytes) + '</td>' +
      '<td style="font-size:12px;color:#94a3b8;font-family:monospace">' + (r.topMethod || '–') + '</td>' +
      '<td style="font-size:11px;color:#94a3b8;white-space:nowrap">' + (r.lastSeen || '–') + '</td>' +
      '<td>' +
        (isBlocked
          ? '<span class="badge error" style="margin-right:4px">Blocked</span>' +
            '<button class="btn-neutral" style="font-size:11px" onclick="unblockIP(\'' + r.ip + '\')">Unblock</button>'
          : '<button class="btn-stop" style="font-size:11px" onclick="openBlockIP(\'' + r.ip + '\')">Block</button>') +
      '</td>' +
    '</tr>';
  }).join('');

  var blockedRows = _blockedIPs.map(function(b) {
    return '<tr>' +
      '<td class="b" style="font-family:monospace;font-size:12px">' + b.ip + '</td>' +
      '<td style="font-size:12px;color:#64748b">' + b.name + '</td>' +
      '<td><span class="badge ' + (b.enabled ? 'error' : 'stopped') + '">' + (b.enabled ? 'Active' : 'Disabled') + '</span></td>' +
      '<td><button class="btn-neutral" style="font-size:11px" onclick="unblockIP(\'' + b.ip.split(',')[0].trim() + '\')">Remove</button></td>' +
    '</tr>';
  }).join('');

  c.innerHTML =
    '<div class="card" style="margin-bottom:16px">' +
      '<div class="search-row">' +
        '<h3>📡 Incoming Traffic <span style="font-size:12px;color:#94a3b8;font-weight:normal">from IIS access logs</span></h3>' +
        '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
          '<input id="traffic-search" placeholder="Filter IP / country..." value="' + _trafficFilter + '" ' +
            'oninput="_trafficFilter=this.value;renderTraffic(el(\'content\'))" ' +
            'style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px;width:160px" />' +
          '<select onchange="_trafficSort=this.value;renderTraffic(el(\'content\'))" ' +
            'style="font-size:12px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px">' +
            '<option value="hits"' + (_trafficSort==='hits'?' selected':'') + '>Sort: Hits</option>' +
            '<option value="bytes"' + (_trafficSort==='bytes'?' selected':'') + '>Sort: Bytes</option>' +
          '</select>' +
          '<button class="btn-neutral" onclick="loadTraffic(el(\'content\'))">&#8635; Refresh</button>' +
          '<button class="btn-danger" onclick="openBlockIP(\'\')">+ Block IP</button>' +
        '</div>' +
      '</div>' +
      (sorted.length === 0
        ? '<div style="text-align:center;padding:40px 0;color:#94a3b8;font-size:13px">No traffic data found. IIS logs may be empty or in a non-standard location.</div>'
        : '<div style="overflow-x:auto"><table>' +
            '<thead><tr>' +
              '<th>IP Address</th><th>Country</th>' +
              '<th style="text-align:right">Hits</th>' +
              '<th style="text-align:right">Bytes</th>' +
              '<th>Top Method</th><th>Last Seen</th><th>Action</th>' +
            '</tr></thead>' +
            '<tbody>' + trafficRows + '</tbody>' +
          '</table></div>' +
          '<p style="font-size:11px;color:#94a3b8;margin-top:8px">' + sorted.length + ' unique IPs' + (_trafficFilter ? ' (filtered)' : '') + ' &mdash; geo data via ip-api.com</p>') +
    '</div>' +
    '<div class="card">' +
      '<div class="search-row"><h3>&#128683; Blocked IPs</h3>' +
        '<button class="btn-primary" style="font-size:11px" onclick="openBlockIP(\'\')">+ Block IP</button>' +
      '</div>' +
      (_blockedIPs.length === 0
        ? '<p style="color:#94a3b8;font-size:13px">No IPs currently blocked by ServerPanel firewall rules.</p>'
        : '<table><thead><tr><th>IP / Range</th><th>Rule Name</th><th>Status</th><th>Action</th></tr></thead>' +
          '<tbody>' + blockedRows + '</tbody></table>') +
    '</div>';
}

function countryFlag(cc) {
  if (!cc || cc.length !== 2) return '';
  // Convert country code to regional indicator emoji
  var offset = 127397;
  return String.fromCodePoint(cc.charCodeAt(0) + offset) +
         String.fromCodePoint(cc.charCodeAt(1) + offset);
}

function openBlockIP(ip) {
  el('bip-ip').value     = ip || '';
  el('bip-reason').value = '';
  hideErr('bip-err');
  om('m-blockip');
}

function submitBlockIP() {
  hideErr('bip-err');
  var ip       = el('bip-ip').value.trim();
  var reason   = el('bip-reason').value.trim();
  var fromTab  = el('bip-ip').dataset.fromTab === '1';
  if (!ip) { showErr('bip-err', 'IP address is required'); return; }
  api('/api/traffic/block', 'POST', { ip: ip, reason: reason }).then(function(r) {
    if (r.error) { showErr('bip-err', r.error); return; }
    cm('m-blockip');
    cacheClear('traffic');
    el('bip-ip').dataset.fromTab = '';
    if (fromTab) renderTrafficTab(el('content'));
    else         loadTraffic(el('content'));
  }).catch(function(e) { showErr('bip-err', e.message); });
}

function unblockIP(ip) {
  if (!confirm('Remove firewall block for ' + ip + '?')) return;
  api('/api/traffic/block', 'DELETE', { ip: ip }).then(function(r) {
    if (r.error) { alert(r.error); return; }
    cacheClear('traffic');
    loadTraffic(el('content'));
  });
}



// ── .NET Manager ───────────────────────────────────────────
var _dotnetPollers = {}; // id -> interval handle

// ── 2. .NET MANAGER ────────────────────────────────────────
function loadDotnet(c) {
  // Skeleton: status bar + two-column layout
  c.innerHTML =
    '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:16px">' +
      '<div class="sk-line" style="width:16px;height:16px;border-radius:50%"></div>' +
      '<div class="sk-line" style="width:200px;height:14px"></div>' +
      '<div style="margin-left:auto"><div class="sk-line" style="width:80px;height:28px;border-radius:6px"></div></div>' +
    '</div>' +
    '<div class="two-col">' +
      '<div>' +
        skeletonCard('.NET Framework (Installed)', ['Version','Service Pack','Type'], 3) +
        skeletonCard('.NET SDKs (Installed)',       ['SDK Version','Path'], 3) +
        skeletonCard('.NET Runtimes (Installed)',   ['Runtime','Version','Path'], 3) +
      '</div>' +
      '<div>' +
        skeletonCard('⚡ .NET SDK',         ['Package','Status'], 2) +
        skeletonCard('🌐 ASP.NET Runtime',  ['Package','Status'], 2) +
        skeletonCard('🔷 .NET Framework',   ['Package','Status'], 2) +
      '</div>' +
    '</div>';

  var cached = cacheGet('dotnet');
  if (cached) { renderDotnet(c, cached); return; }

  api('/api/dotnet').then(function(data) {
    cacheSet('dotnet', data);
    renderDotnet(c, data);
  }).catch(function() {
    c.innerHTML = '<div class="err-box">Could not reach backend.</div>';
  });
}

function renderDotnet(c, data) {
  // ── .NET Framework card ──────────────────────────────────
  var fwRows = '';
  if (!data.framework || !data.framework.length) {
    fwRows = '<p style="color:#94a3b8;font-size:13px;padding:10px 0">No .NET Framework versions detected.</p>';
  } else {
    // Deduplicate by version label
    var seen = {};
    var unique = data.framework.filter(function(f) {
      if (seen[f.version]) return false;
      seen[f.version] = true;
      return true;
    });
    fwRows = '<table><thead><tr><th>Version</th><th>Service Pack</th><th>Type</th></tr></thead><tbody>' +
      unique.map(function(f) {
        return '<tr>' +
          '<td class="b">.NET Framework ' + f.version + '</td>' +
          '<td style="font-size:12px;color:#64748b">' + (f.sp || '—') + '</td>' +
          '<td><span class="badge info" style="font-size:11px">Framework</span></td>' +
        '</tr>';
      }).join('') +
    '</tbody></table>';
  }

  // ── .NET Core / 5+ SDKs card ─────────────────────────────
  var sdkRows = '';
  if (!data.sdks || !data.sdks.length) {
    sdkRows = '<p style="color:#94a3b8;font-size:13px;padding:10px 0">No .NET SDKs detected. dotnet CLI may not be on PATH.</p>';
  } else {
    sdkRows = '<table><thead><tr><th>SDK Version</th><th>Path</th></tr></thead><tbody>' +
      data.sdks.map(function(s) {
        var major = parseInt(s.version);
        var isLts = [6, 8].includes(major);
        var isLatest = major >= 9;
        var badge = isLatest
          ? '<span class="badge running" style="font-size:10px;margin-left:6px">Latest</span>'
          : isLts
            ? '<span class="badge info" style="font-size:10px;margin-left:6px">LTS</span>'
            : '';
        return '<tr>' +
          '<td class="b">' + s.version + badge + '</td>' +
          '<td style="font-size:12px;color:#64748b;font-family:monospace">' + (s.path || '—') + '</td>' +
        '</tr>';
      }).join('') +
    '</tbody></table>';
  }

  // ── Runtimes card ────────────────────────────────────────
  var rtRows = '';
  if (!data.runtimes || !data.runtimes.length) {
    rtRows = '<p style="color:#94a3b8;font-size:13px;padding:10px 0">No .NET runtimes detected.</p>';
  } else {
    rtRows = '<table><thead><tr><th>Runtime</th><th>Version</th><th>Path</th></tr></thead><tbody>' +
      data.runtimes.map(function(r) {
        return '<tr>' +
          '<td class="b" style="font-size:12px">' + (r.name || '—') + '</td>' +
          '<td style="font-size:12px;font-weight:500">' + (r.version || '—') + '</td>' +
          '<td style="font-size:11px;color:#64748b;font-family:monospace">' + (r.path || '—') + '</td>' +
        '</tr>';
      }).join('') +
    '</tbody></table>';
  }

  // ── Install / upgrade cards ──────────────────────────────
  var installable = data.installable || [];
  var installStatus = data.installStatus || {};

  // Group by type
  var groups = {
    dotnet:    { label: '⚡ .NET SDK (Core / 5+)', items: [] },
    aspnet:    { label: '🌐 ASP.NET Core Runtime',  items: [] },
    framework: { label: '🔷 .NET Framework',        items: [] }
  };
  installable.forEach(function(pkg) {
    if (groups[pkg.type]) groups[pkg.type].items.push(pkg);
  });

  var installCards = Object.keys(groups).map(function(type) {
    var group = groups[type];
    var items = group.items.map(function(pkg) {
      var st = installStatus[pkg.id] || { status: 'idle' };
      var isRunning = st.status === 'running';
      var isDone    = st.status === 'done';
      var isErr     = st.status === 'error';

      var statusBadge = isRunning
        ? '<span class="badge warn" style="font-size:10px">Installing...</span>'
        : isDone
          ? '<span class="badge running" style="font-size:10px">Installed ✓</span>'
          : isErr
            ? '<span class="badge error" style="font-size:10px">Failed</span>'
            : '';

      var latestBadge = pkg.latest
        ? '<span class="badge info" style="font-size:10px;margin-left:4px">Recommended</span>'
        : '';

      var btnInstall = '<button class="btn-primary" style="font-size:11px;padding:4px 12px" ' +
        (isRunning ? 'disabled' : '') +
        ' id="dotnet-btn-' + pkg.id + '" onclick="dotnetInstall(\'' + pkg.id + '\',\'' + pkg.label.replace(/'/g,"\\'") + '\')">' +
        (isRunning ? 'Installing...' : 'Install / Upgrade') + '</button>';

      var logLine = (isRunning || isDone || isErr) && st.log
        ? '<div style="font-size:11px;color:#64748b;font-family:monospace;margin-top:4px;white-space:pre-wrap">' + st.log.substring(0, 200) + '</div>'
        : '';

      return '<div style="display:flex;align-items:flex-start;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f8fafc;gap:10px">' +
        '<div>' +
          '<div style="font-size:13px;font-weight:500">' + pkg.label + latestBadge + ' ' + statusBadge + '</div>' +
          '<div style="font-size:11px;color:#94a3b8;margin-top:2px;font-family:monospace">choco install ' + pkg.choco + '</div>' +
          logLine +
        '</div>' +
        '<div style="flex-shrink:0">' + btnInstall + '</div>' +
      '</div>';
    }).join('');

    return '<div class="card">' +
      '<h3 style="margin-bottom:14px">' + group.label + '</h3>' +
      items +
    '</div>';
  }).join('');

  // ── dotnet CLI info bar ──────────────────────────────────
  var cliBar = data.dotnetPath
    ? '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;background:#f0fdf4;border:1px solid #86efac;border-radius:8px;margin-bottom:16px">' +
        '<span style="color:#16a34a;font-size:16px">✓</span>' +
        '<div><div style="font-size:13px;font-weight:500;color:#166534">dotnet CLI detected</div>' +
        '<div style="font-size:11px;color:#16a34a;font-family:monospace">' + data.dotnetPath + '</div></div>' +
        '<button class="btn-neutral" style="margin-left:auto;font-size:11px" onclick="loadDotnet(el(\'content\'))">↻ Refresh</button>' +
      '</div>'
    : '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;margin-bottom:16px">' +
        '<span style="color:#d97706;font-size:16px">⚠</span>' +
        '<div style="font-size:13px;color:#92400e">' +
          'dotnet CLI not found on PATH. SDK / runtime detection may be incomplete. Install a .NET SDK below.' +
        '</div>' +
        '<button class="btn-neutral" style="margin-left:auto;font-size:11px" onclick="loadDotnet(el(\'content\'))">↻ Refresh</button>' +
      '</div>';

  c.innerHTML =
    cliBar +
    '<div class="two-col">' +
      '<div>' +
        '<div class="card"><h3 style="margin-bottom:12px">🔷 .NET Framework (Installed)</h3>' + fwRows + '</div>' +
        '<div class="card"><h3 style="margin-bottom:12px">⚡ .NET SDKs (Installed)</h3>' + sdkRows + '</div>' +
        '<div class="card"><h3 style="margin-bottom:12px">🌐 .NET Runtimes (Installed)</h3>' + rtRows + '</div>' +
      '</div>' +
      '<div>' + installCards + '</div>' +
    '</div>';
}

function dotnetInstall(id, label) {
  if (!confirm('Install / upgrade "' + label + '"?\n\nThis uses Chocolatey and may take 5–15 minutes.\nThe page will poll for progress automatically.')) return;

  var btn = el('dotnet-btn-' + id);
  if (btn) { btn.textContent = 'Starting...'; btn.disabled = true; }

  api('/api/dotnet/install', 'POST', { id: id }).then(function(r) {
    if (!r.ok) { alert(r.error || 'Failed to start install'); if (btn) { btn.textContent = 'Install / Upgrade'; btn.disabled = false; } return; }

    // Poll for status every 8 seconds
    _dotnetPollers[id] = setInterval(function() {
      api('/api/dotnet/status/' + id).then(function(st) {
        var btn2 = el('dotnet-btn-' + id);
        if (st.status === 'done') {
          clearInterval(_dotnetPollers[id]);
          delete _dotnetPollers[id];
          if (btn2) btn2.textContent = 'Install / Upgrade';
          alert('"' + label + '" installed successfully! Refreshing...');
          loadDotnet(el('content'));
        } else if (st.status === 'error') {
          clearInterval(_dotnetPollers[id]);
          delete _dotnetPollers[id];
          if (btn2) { btn2.textContent = 'Install / Upgrade'; btn2.disabled = false; }
          alert('Installation failed:\n' + (st.log || 'Unknown error'));
        } else if (st.status === 'running') {
          if (btn2) btn2.textContent = 'Installing...';
        }
      }).catch(function() {});
    }, 8000);

  }).catch(function(e) {
    alert('Error: ' + e.message);
    if (btn) { btn.textContent = 'Install / Upgrade'; btn.disabled = false; }
  });
}

// ── Docker Manager ─────────────────────────────────────────
var _dockerTab = 'containers';
var _dockerInstallPoller = null;

// ── 4. DOCKER ──────────────────────────────────────────────
function loadDocker(c) {
  // Skeleton stays — shown immediately
c.innerHTML =
    '<div style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:16px">' +
      '<div class="sk-line" style="width:24px;height:24px;border-radius:50%;flex-shrink:0"></div>' +
      '<div>' +
        '<div class="sk-line" style="width:140px;height:13px;margin-bottom:6px"></div>' +
        '<div class="sk-line" style="width:100px;height:11px"></div>' +
      '</div>' +
      '<div style="margin-left:auto;display:flex;gap:8px">' +
        '<div class="sk-line" style="width:70px;height:28px;border-radius:6px"></div>' +
        '<div class="sk-line" style="width:90px;height:28px;border-radius:6px"></div>' +
      '</div>' +
    '</div>' +
    '<div style="display:flex;gap:8px;margin-bottom:16px">' +
      '<div class="sk-line" style="width:90px;height:32px;border-radius:6px"></div>' +
      '<div class="sk-line" style="width:70px;height:32px;border-radius:6px"></div>' +
    '</div>' +
    skeletonCard('Containers', ['ID','Name','Image','Status','Ports','Actions'], 4);

  // ← ADD cache check
  var cached = cacheGet('docker');
  if (cached) { 
    if (!cached.installed || !cached.running) renderDockerInstall(c, cached);
    else renderDockerPanel(c, cached);
    return;
  }

  api('/api/docker/status').then(function(status) {
    cacheSet('docker', status);   // ← ADD
    if (!status.installed || !status.running) {
      renderDockerInstall(c, status);
    } else {
      renderDockerPanel(c, status);
    }
  }).catch(function() {
    c.innerHTML = '<div class="err-box">Could not reach backend.</div>';
  });
}

// ── Not installed — show install screen ───────────────────
function renderDockerInstall(c, status) {
  var isInstalled = status.installed;
  c.innerHTML =
    '<div class="card" style="max-width:560px">' +
      '<div style="text-align:center;padding:20px 0 10px">' +
        '<div style="font-size:48px;margin-bottom:12px">🐳</div>' +
        '<h3 style="font-size:18px;margin-bottom:8px">' + (isInstalled ? 'Docker is not running' : 'Docker is not installed') + '</h3>' +
        '<p style="font-size:13px;color:#64748b;margin-bottom:20px">' +
          (isInstalled
            ? 'Docker Desktop is installed but not running. Start it from the system tray.'
            : 'Install Docker Desktop to manage containers directly from this panel.') +
        '</p>' +
      '</div>' +
      (isInstalled ? '' :
        '<div style="background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0;padding:16px;margin-bottom:16px">' +
          '<div style="font-size:12px;font-weight:500;color:#475569;margin-bottom:8px">What gets installed</div>' +
          '<div style="font-size:13px;color:#64748b;line-height:1.8">' +
            '• Docker Desktop (via Chocolatey)<br>' +
            '• Docker Engine + CLI<br>' +
            '• Docker Compose<br>' +
            '• WSL2 integration (if available)' +
          '</div>' +
        '</div>') +
      '<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">' +
        (isInstalled
          ? '<button class="btn-primary" onclick="loadDocker(el(\'content\'))">↻ Check again</button>'
          : '<button class="btn-primary" id="docker-install-btn" onclick="startDockerInstall()">🐳 Install Docker Desktop</button>') +
        '<a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="btn-neutral" style="text-decoration:none;padding:5px 14px">⬇ Download manually</a>' +
      '</div>' +
      '<div id="docker-install-status" style="margin-top:14px;text-align:center"></div>' +
    '</div>';

  // Check if install was already in progress
  api('/api/docker/install-status').then(function(st) {
    if (st.status === 'running') pollDockerInstall();
  }).catch(function() {});
}

function startDockerInstall() {
  var btn = el('docker-install-btn');
  btn.textContent = 'Installing...'; btn.disabled = true;
  api('/api/docker/install', 'POST').then(function(r) {
    if (!r.ok) { alert(r.error || 'Failed'); btn.textContent = 'Install Docker Desktop'; btn.disabled = false; return; }
    pollDockerInstall();
  }).catch(function(e) { alert(e.message); btn.textContent = 'Install Docker Desktop'; btn.disabled = false; });
}

function pollDockerInstall() {
  var statusEl = el('docker-install-status');
  if (statusEl) statusEl.innerHTML = '<span style="font-size:12px;color:#64748b">Installing... this may take 10–20 minutes. You can leave this page.</span>';
  if (_dockerInstallPoller) clearInterval(_dockerInstallPoller);
  _dockerInstallPoller = setInterval(function() {
    api('/api/docker/install-status').then(function(st) {
      if (st.status === 'done') {
        clearInterval(_dockerInstallPoller);
        alert('Docker Desktop installed! Please restart the server machine, then come back to this page.');
        loadDocker(el('content'));
      } else if (st.status === 'error') {
        clearInterval(_dockerInstallPoller);
        var s = el('docker-install-status');
        if (s) s.innerHTML = '<span style="font-size:12px;color:#dc2626">Installation failed: ' + (st.log || 'Unknown error') + '</span>';
      }
    }).catch(function() {});
  }, 10000);
}

// ── Docker running — show full panel ─────────────────────
function renderDockerPanel(c, status) {
  c.innerHTML =
    '<div style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:#f0fdf4;border:1px solid #86efac;border-radius:8px;margin-bottom:16px">' +
      '<span style="font-size:20px">🐳</span>' +
      '<div><div style="font-size:13px;font-weight:500;color:#166534">Docker is running</div>' +
      '<div style="font-size:11px;color:#16a34a">' + (status.version || '') + '</div></div>' +
      '<button class="btn-neutral" style="margin-left:auto;font-size:11px" onclick="cacheClear(\'docker\');loadDocker(el(\'content\'))">↻ Refresh</button>' +
      '<button class="btn-primary" style="font-size:11px" onclick="om(\'m-docker-run\')">+ Run container</button>' +
      '<button class="btn-neutral" style="font-size:11px" onclick="om(\'m-docker-pull\')">⬇ Pull image</button>' +
    '</div>' +
    '<div class="tabs" style="margin-bottom:16px">' +
      '<div class="tab' + (_dockerTab==='containers'?' active':'') + '" onclick="dockerSwitchTab(\'containers\')">Containers</div>' +
      '<div class="tab' + (_dockerTab==='images'?' active':'') + '" onclick="dockerSwitchTab(\'images\')">Images</div>' +
    '</div>' +
    '<div id="docker-tab-body"></div>';

  dockerLoadTab(_dockerTab);
}

function dockerSwitchTab(tab) {
  _dockerTab = tab;
  document.querySelectorAll('.tab').forEach(function(t) {
    t.classList.toggle('active',
      (tab==='containers' && t.textContent==='Containers') ||
      (tab==='images'     && t.textContent==='Images'));
  });
  dockerLoadTab(tab);
}

function dockerLoadTab(tab) {
  var body = el('docker-tab-body'); if (!body) return;
  body.innerHTML = '<div class="loading">Loading...</div>';
  if (tab === 'containers') dockerLoadContainers(body);
  if (tab === 'images')     dockerLoadImages(body);
}

function dockerLoadContainers(body) {
  api('/api/docker/containers').then(function(list) {
    if (!list.length) {
      body.innerHTML = '<div class="placeholder"><div class="ph-icon">📦</div><h3>No containers</h3><p>Run a container using the button above.</p></div>';
      return;
    }
    var rows = list.map(function(ct) {
      var isUp  = (ct.State || ct.Status || '').toLowerCase().includes('up');
      var name  = (ct.Names || ct.Name || '').replace(/^\//, '');
      var id    = ct.ID || ct.Id || '';
      var image = ct.Image || '';
      var ports = ct.Ports || '';
      var state = ct.State || ct.Status || '';
      return '<tr>' +
        '<td class="m" style="font-size:11px">' + id.substring(0,12) + '</td>' +
        '<td class="b">' + name + '</td>' +
        '<td style="font-size:12px;color:#64748b">' + image + '</td>' +
        '<td><span class="badge ' + (isUp?'running':'stopped') + '"><span class="bdot"></span>' + state + '</span></td>' +
        '<td style="font-size:11px;color:#94a3b8;font-family:monospace">' + ports + '</td>' +
        '<td style="white-space:nowrap">' +
          (isUp
            ? '<button class="btn-stop" onclick="dockerAction(\'' + id + '\',\'stop\')">Stop</button>'
            : '<button class="btn-start" onclick="dockerAction(\'' + id + '\',\'start\')">Start</button>') +
          '<button class="btn-neutral" onclick="dockerAction(\'' + id + '\',\'restart\')">↻</button>' +
          '<button class="btn-neutral" onclick="dockerLogs(\'' + id + '\',\'' + name + '\')">Logs</button>' +
          '<button class="btn-danger" onclick="dockerAction(\'' + id + '\',\'remove\')">✕</button>' +
        '</td>' +
      '</tr>';
    }).join('');
    body.innerHTML =
      '<div class="card">' +
        '<table><thead><tr><th>ID</th><th>Name</th><th>Image</th><th>Status</th><th>Ports</th><th>Actions</th></tr></thead>' +
        '<tbody>' + rows + '</tbody></table>' +
      '</div>';
  }).catch(function(e) { body.innerHTML = '<div class="err-box">' + e.message + '</div>'; });
}

function dockerLoadImages(body) {
  api('/api/docker/images').then(function(list) {
    if (!list.length) {
      body.innerHTML = '<div class="placeholder"><div class="ph-icon">🖼️</div><h3>No images</h3><p>Pull an image using the button above.</p></div>';
      return;
    }
    var rows = list.map(function(img) {
      var id  = img.ID || img.Id || '';
      var rep = img.Repository || '';
      var tag = img.Tag || '';
      var sz  = img.Size || '';
      var cr  = img.CreatedSince || img.Created || '';
      return '<tr>' +
        '<td class="b">' + rep + '</td>' +
        '<td><span class="badge info" style="font-size:11px">' + tag + '</span></td>' +
        '<td class="m" style="font-size:11px">' + id.substring(0,12) + '</td>' +
        '<td style="font-size:12px;color:#64748b">' + sz + '</td>' +
        '<td style="font-size:12px;color:#94a3b8">' + cr + '</td>' +
        '<td>' +
          '<button class="btn-primary" style="font-size:11px" onclick="dockerRunFromImage(\'' + rep + ':' + tag + '\')">▶ Run</button>' +
          '<button class="btn-danger" onclick="dockerRemoveImage(\'' + id + '\')">✕</button>' +
        '</td>' +
      '</tr>';
    }).join('');
    body.innerHTML =
      '<div class="card">' +
        '<table><thead><tr><th>Repository</th><th>Tag</th><th>Image ID</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>' +
        '<tbody>' + rows + '</tbody></table>' +
      '</div>';
  }).catch(function(e) { body.innerHTML = '<div class="err-box">' + e.message + '</div>'; });
}

function dockerAction(id, action) {
  if (action === 'remove' && !confirm('Remove container ' + id + '?')) return;
  var endpoint = action === 'remove'
    ? { url: '/api/docker/containers/' + id, method: 'DELETE' }
    : { url: '/api/docker/containers/' + id + '/' + action, method: 'POST' };
  api(endpoint.url, endpoint.method).then(function() {
    dockerLoadTab(_dockerTab);
  }).catch(function(e) { alert('Error: ' + e.message); });
}

function dockerRemoveImage(id) {
  if (!confirm('Remove image ' + id + '?')) return;
  api('/api/docker/images/' + id, 'DELETE').then(function() {
    dockerLoadTab('images');
  }).catch(function(e) { alert('Error: ' + e.message); });
}

function dockerRunFromImage(image) {
  el('dr-image').value = image;
  el('dr-name').value  = '';
  el('dr-ports').value = '';
  el('dr-env').value   = '';
  hideErr('dr-err');
  om('m-docker-run');
}

function submitDockerRun() {
  hideErr('dr-err');
  var image = val('dr-image').trim();
  if (!image) { showErr('dr-err', 'Image name is required'); return; }
  var ports = val('dr-ports').trim().split('\n').map(function(p){return p.trim();}).filter(Boolean);
  var env   = val('dr-env').trim().split('\n').map(function(e){return e.trim();}).filter(Boolean);
  api('/api/docker/containers/run', 'POST', {
    image: image, name: val('dr-name').trim(), ports: ports, env: env, detach: true
  }).then(function(r) {
    if (r.error) { showErr('dr-err', r.error); return; }
    cm('m-docker-run');
    setTimeout(function() { dockerLoadTab('containers'); }, 1000);
  }).catch(function(e) { showErr('dr-err', e.message); });
}

function submitDockerPull() {
  hideErr('dp-err');
  var image = val('dp-image').trim();
  if (!image) { showErr('dp-err', 'Image name is required'); return; }
  api('/api/docker/images/pull', 'POST', { image: image }).then(function(r) {
    if (r.error) { showErr('dp-err', r.error); return; }
    cm('m-docker-pull');
    alert('Pulling ' + image + ' in background. Refresh Images tab in a minute.');
  }).catch(function(e) { showErr('dp-err', e.message); });
}

function dockerLogs(id, name) {
  var win = window.open('', '_blank', 'width=800,height=500');
 win.document.write('<html><head><title>Logs \u2014 ' + name + '<\/title><style>body{background:#0f172a;color:#e2e8f0;font-family:monospace;font-size:13px;padding:16px;white-space:pre-wrap;word-break:break-all}<\/style><\/head><body>Loading logs...<\/body><\/html>');
  api('/api/docker/containers/' + id + '/logs?lines=200').then(function(r) {
    win.document.body.textContent = r.logs || 'No logs available.';
  }).catch(function(e) { win.document.body.textContent = 'Error: ' + e.message; });
}


// ── Data Backup ────────────────────────────────────────────
function triggerBackup() {
  var btn = el('backup-btn');
  if (btn) { btn.textContent = '💾 Backing up...'; btn.disabled = true; }
  api('/api/backup', 'POST').then(function(r) {
    if (btn) { btn.textContent = '💾 Backup'; btn.disabled = false; }
    if (r.error) { alert('Backup failed: ' + r.error); return; }
    alert('✓ Backup created: ' + r.file + '\n(' + formatBytes(r.size) + ')');
    // Refresh list if modal is open
    var modal = el('m-backups');
    if (modal && modal.style.display !== 'none') loadBackupList();
  }).catch(function(e) {
    if (btn) { btn.textContent = '💾 Backup'; btn.disabled = false; }
    alert('Error: ' + e.message);
  });
}

function openBackupsModal() {
  om('m-backups');
  loadBackupList();
}

function loadBackupList() {
  var body = el('backup-list-body'); if (!body) return;
  body.innerHTML = '<div class="loading">Loading...</div>';
  api('/api/backup/list').then(function(list) {
    if (!list.length) {
      body.innerHTML = '<div style="text-align:center;padding:30px 0;color:#94a3b8;font-size:13px">No backups yet. Click "Create backup now" to make one.</div>';
      return;
    }
    var rows = list.map(function(b) {
      return '<tr>' +
        '<td class="b" style="font-size:12px;font-family:monospace">' + b.name + '</td>' +
        '<td style="font-size:12px;color:#64748b">' + formatBytes(b.size) + '</td>' +
        '<td style="font-size:12px;color:#94a3b8">' + new Date(b.created).toLocaleString() + '</td>' +
        '<td>' +
          '<a href="' + 'http://localhost:4080/api/files/download?path=' + encodeURIComponent('D:\\panel\\data\\backups\\' + b.name) + '" ' +
            'onclick="return downloadBackup(\'' + b.name + '\')" ' +
            'class="btn-neutral" style="text-decoration:none;font-size:11px;margin-left:0">⬇ Download</a>' +
          '<button class="btn-danger" onclick="deleteBackup(\'' + b.name + '\')">✕</button>' +
        '</td>' +
      '</tr>';
    }).join('');
    body.innerHTML =
      '<table><thead><tr><th>File</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>' +
      '<tbody>' + rows + '</tbody></table>' +
      '<p style="font-size:11px;color:#94a3b8;margin-top:10px">' + list.length + ' backup(s) stored on server</p>';
  }).catch(function() { body.innerHTML = '<div class="err-box">Could not load backups.</div>'; });
}

function downloadBackup(name) {
  var url = API + '/api/files/download?path=' + encodeURIComponent('D:\\panel\\data\\backups\\' + name);
  fetch(url, { headers: { Authorization: 'Bearer ' + TOKEN } })
    .then(function(r) {
      return r.blob().then(function(blob) {
        var a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = name;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      });
    }).catch(function(e) { alert('Download failed: ' + e.message); });
  return false;
}

function deleteBackup(name) {
  if (!confirm('Delete backup "' + name + '"? This cannot be undone.')) return;
  api('/api/backup/' + encodeURIComponent(name), 'DELETE').then(function(r) {
    if (r.error) { alert(r.error); return; }
    loadBackupList();
  });
}

// ── Bootstrap ─────────────────────────────────────────────
document.addEventListener('click', function(e) {
  if (e.target.classList.contains('modal-bg')) e.target.style.display = 'none';
});
el('inp-pass').addEventListener('keydown', function(e) {
  if (e.key === 'Enter') doLogin();
});

// ════════════════════════════════════════════════
//  BACKUP MANAGER
// ════════════════════════════════════════════════

var curGitDetailRepo = '';
var curGitDetailTab  = 'log';
 
function loadBackup(c) {
  // Skeleton shown immediately
  c.innerHTML =
    '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
      '<div class="sk-line" style="width:280px;height:13px"></div>' +
      '<div class="sk-line" style="width:120px;height:34px;border-radius:6px"></div>' +
    '</div>' +
    '<div class="card">' +
      '<table><thead><tr>' +
        '<th>Filename</th><th>Type</th><th>Size</th><th>Created</th><th style="text-align:right">Actions</th>' +
      '</tr></thead><tbody>' +
      [1,2,3,4,5].map(function() {
        return '<tr>' +
          '<td><div class="sk-line" style="width:200px;height:12px"></div></td>' +
          '<td><div class="sk-line" style="width:40px;height:20px;border-radius:4px"></div></td>' +
          '<td><div class="sk-line" style="width:60px;height:12px"></div></td>' +
          '<td><div class="sk-line" style="width:120px;height:12px"></div></td>' +
          '<td style="text-align:right;display:flex;gap:6px;justify-content:flex-end">' +
            '<div class="sk-line" style="width:80px;height:28px;border-radius:6px"></div>' +
            '<div class="sk-line" style="width:70px;height:28px;border-radius:6px"></div>' +
          '</td>' +
        '</tr>';
      }).join('') +
      '</tbody></table>' +
    '</div>';

  // Show cache instantly if available
  var cached = cacheGet('backups');
  if (cached) { renderBackupList(c, cached); return; }

  api('/api/backups/list').then(function(d) {
    if (!d.ok) {
      if (d.error && d.error.includes('Cannot find module')) {
        c.innerHTML = missingModuleBanner(['archiver', 'simple-git'], 'backup');
        return;
      }
      c.innerHTML = '<div class="err-box">' + d.error + '</div>';
      return;
    }
    cacheSet('backups', d);
    renderBackupList(c, d);
  });
}

function renderBackupList(c, d) {
  BACKUP_DIR_LABEL = d.dir || 'D:\\panel\\backups';
  var totalSize = d.backups.reduce(function(s, b) { return s + b.size; }, 0);

  var html =
    '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
      '<div>' +
        '<div style="font-size:13px;color:var(--text-muted)">' +
          'Storage: <b>' + BACKUP_DIR_LABEL + '</b> &nbsp;|&nbsp; ' +
          d.backups.length + ' backup' + (d.backups.length !== 1 ? 's' : '') +
          ' &nbsp;|&nbsp; Total: <b>' + formatBytes(totalSize) + '</b>' +
        '</div>' +
      '</div>' +
      '<div style="display:flex;gap:8px">' +
        '<button class="btn-neutral" onclick="cacheClear(\'backups\');loadBackup(el(\'content\'))">&#8635; Refresh</button>' +
        '<button class="btn-primary" onclick="om(\'m-backup-create\')">+ Create Backup</button>' +
      '</div>' +
    '</div>';

  if (d.backups.length === 0) {
    html += '<div class="empty-state"><div style="font-size:40px">🗄️</div><p>No backups yet</p><p style="font-size:12px;color:var(--text-muted)">Click "Create Backup" to get started</p></div>';
  } else {
    html += '<table class="data-table"><thead><tr>' +
      '<th>Filename</th><th>Type</th><th>Size</th><th>Created</th><th style="text-align:right">Actions</th>' +
      '</tr></thead><tbody>';

    d.backups.forEach(function(b) {
      html += '<tr>' +
        '<td style="font-family:monospace;font-size:12px">' + escHtml(b.name) + '</td>' +
        '<td><span class="badge badge-blue">' + escHtml(b.type) + '</span></td>' +
        '<td>' + formatBytes(b.size) + '</td>' +
        '<td>' + formatDate(b.created) + '</td>' +
        '<td style="text-align:right">' +
          '<button class="btn-ghost" style="margin-right:6px;font-size:12px" onclick="downloadBackup(\'' + b.name.replace(/'/g, "\\'") + '\')">⬇ Download</button>' +
          '<button class="btn-danger" style="font-size:12px" onclick="deleteBackup(\'' + b.name.replace(/'/g, "\\'") + '\')">🗑 Delete</button>' +
        '</td>' +
      '</tr>';
    });

    html += '</tbody></table>';
  }

  c.innerHTML = html;
}

// BUG 2 FIX — helper to download with Authorization header
function downloadBackup(filename) {
  fetch(API + '/api/backups/download/' + encodeURIComponent(filename), {
    headers: { Authorization: 'Bearer ' + TOKEN }
  })
  .then(function(r) {
    if (!r.ok) { alert('Download failed — server returned ' + r.status); return null; }
    return r.blob();
  })
  .then(function(blob) {
    if (!blob) return;
    var url = URL.createObjectURL(blob);
    var a   = document.createElement('a');
    a.href  = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  })
  .catch(function() { alert('Download failed'); });
}
 
var BACKUP_DIR_LABEL = 'D:\\panel\\backups';
 
function submitCreateBackup() {
  var name   = val('bk-name').trim();
  var source = val('bk-source').trim();
  var note   = val('bk-note').trim();
  if (!name || !source) { showErr('err-backup-create', 'Name and source path are required'); return; }
 
  hideErr('err-backup-create');
  var btn = document.querySelector('#m-backup-create .btn-primary');
  btn.textContent = 'Creating...'; btn.disabled = true;
 
  api('/api/backups/create', 'POST', { name: name, source: source, note: note }).then(function(d) {
    btn.textContent = 'Create Backup'; btn.disabled = false;
    if (!d.ok) { showErr('err-backup-create', d.error); return; }
    cm('m-backup-create');
    el('bk-name').value = ''; el('bk-source').value = ''; el('bk-note').value = '';
    cacheClear('backups');
    showPage('backup', null);
  });
}
 
function deleteBackup(filename) {
  if (!confirm('Delete backup: ' + filename + '?\n\nThis cannot be undone.')) return;
  api('/api/backups/' + encodeURIComponent(filename), 'DELETE').then(function(d) {
    if (!d.ok) { alert('Error: ' + d.error); return; }
    cacheClear('backups');
    showPage('backup', null);
  });
}
 
 
// ════════════════════════════════════════════════
//  GIT REPOSITORIES
// ════════════════════════════════════════════════
 
function loadGit(c) {
    c.innerHTML =
    '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
      '<div class="sk-line" style="width:220px;height:13px"></div>' +
      '<div style="display:flex;gap:8px">' +
        '<div class="sk-line" style="width:100px;height:32px;border-radius:6px"></div>' +
        '<div class="sk-line" style="width:90px;height:32px;border-radius:6px"></div>' +
      '</div>' +
    '</div>' +
    '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px">' +
      [1,2,3,4].map(function() {
        return '<div class="card" style="padding:16px">' +
          '<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">' +
            '<div class="sk-line" style="width:16px;height:16px;border-radius:50%;flex-shrink:0"></div>' +
            '<div class="sk-line" style="width:120px;height:14px"></div>' +
            '<div class="sk-line" style="width:50px;height:20px;border-radius:4px;margin-left:8px"></div>' +
          '</div>' +
          '<div class="sk-line" style="width:80%;height:11px;margin-bottom:6px"></div>' +
          '<div class="sk-line" style="width:60%;height:11px;margin-bottom:14px"></div>' +
          '<div style="display:flex;gap:6px">' +
            [1,2,3,4].map(function() {
              return '<div class="sk-line" style="width:65px;height:26px;border-radius:6px"></div>';
            }).join('') +
          '</div>' +
        '</div>';
      }).join('') +
    '</div>';
    
  api('/api/git/repos').then(function(d) {
    if (!d.ok) {
      if (d.error && d.error.includes('Cannot find module')) {
        c.innerHTML = missingModuleBanner(['archiver', 'simple-git'], 'git');
        return;
      }
      c.innerHTML = '<div class="err-box">' + d.error + '</div>';
      return;
    }
 
    var html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
      '<div style="font-size:13px;color:var(--text-muted)">' +
        'Repos folder: <b style="font-family:monospace">' + (d.dir || 'D:\\panel\\repos') + '</b> &nbsp;|&nbsp; ' +
        d.repos.length + ' repositor' + (d.repos.length !== 1 ? 'ies' : 'y') +
      '</div>' +
      '<div style="display:flex;gap:8px">' +
'<button class="btn-neutral" onclick="om(\'m-git-clone\')">⎇ Clone Repo</button>' +
        '<button class="btn-primary" onclick="om(\'m-git-init\')">+ New Repo</button>' +
      '</div>' +
    '</div>';
 
    if (d.repos.length === 0) {
      html += '<div class="empty-state">' +
        '<div style="font-size:40px">🌿</div>' +
        '<p>No repositories yet</p>' +
        '<p style="font-size:12px;color:var(--text-muted)">Create a new repo or clone an existing one</p>' +
      '</div>';
    } else {
      html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px">';
 
      d.repos.forEach(function(r) {
        var statusDot = r.dirty
          ? '<span style="color:#f59e0b;font-size:11px">● Uncommitted changes (' + r.files + ' file' + (r.files !== 1 ? 's' : '') + ')</span>'
          : '<span style="color:#22c55e;font-size:11px">● Clean</span>';
 
        var lastCommitHtml = r.lastCommit
          ? '<div style="font-size:11px;color:var(--text-muted);margin-top:6px;padding-top:6px;border-top:1px solid var(--border)">' +
              '<span style="color:var(--text)">Last commit:</span> ' +
              escHtml(r.lastCommit.message) +
              ' <span style="color:var(--text-muted)">by ' + escHtml(r.lastCommit.author) + '</span>' +
            '</div>'
          : '';
 
        html += '<div class="card" style="padding:16px">' +
          '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px">' +
            '<div style="flex:1;min-width:0">' +
              '<div style="font-weight:600;font-size:14px;margin-bottom:4px">🌿 ' + escHtml(r.name) + '</div>' +
              '<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px">' +
                '<span class="badge badge-blue">' + escHtml(r.branch) + '</span>' +
                (r.allBranches.length > 1 ? ' <span style="font-size:11px;color:var(--text-muted)">+' + (r.allBranches.length - 1) + ' more</span>' : '') +
              '</div>' +
              statusDot +
              lastCommitHtml +
            '</div>' +
          '</div>' +
 
          '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:12px">' +
            '<button class="btn-ghost" style="font-size:12px" onclick="openGitDetail(\'' + escAttr(r.name) + '\')">📋 Details</button>' +
            (r.dirty
              ? '<button class="btn-ghost" style="font-size:12px" onclick="openGitCommit(\'' + escAttr(r.name) + '\')">✅ Commit</button>'
              : '') +
            '<button class="btn-ghost" style="font-size:12px" onclick="gitPull(\'' + escAttr(r.name) + '\',this)">⬇ Pull</button>' +
            '<button class="btn-ghost" style="font-size:12px" onclick="openGitBranch(\'' + escAttr(r.name) + '\')">🌿 Branch</button>' +
            '<button class="btn-danger" style="font-size:12px" onclick="deleteGitRepo(\'' + escAttr(r.name) + '\')">🗑</button>' +
          '</div>' +
        '</div>';
      });
 
      html += '</div>';
    }
 
    c.innerHTML = html;
  });
}
 
function escHtml(s) {
  return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function escAttr(s) {
  return String(s || '').replace(/'/g,"\\'");
}
 
// ── Init ──────────────────────────────────────────────────
function submitGitInit() {
  var name   = val('git-init-name').trim();
  var desc   = val('git-init-desc').trim();
  var branch = val('git-init-branch').trim() || 'main';
  if (!name) { showErr('err-git-init', 'Repository name is required'); return; }
 
  hideErr('err-git-init');
  var btn = document.querySelector('#m-git-init .btn-primary');
  btn.textContent = 'Initializing...'; btn.disabled = true;
 
  api('/api/git/init', 'POST', { name: name, description: desc, branch: branch }).then(function(d) {
    btn.textContent = 'Initialize'; btn.disabled = false;
    if (!d.ok) { showErr('err-git-init', d.error); return; }
    cm('m-git-init');
    el('git-init-name').value = ''; el('git-init-desc').value = '';
    navigate('git');
  });
}
 
// ── Clone ────────────────────────────────────────────────
function submitGitClone() {
  var url  = val('git-clone-url').trim();
  var name = val('git-clone-name').trim();
  if (!url) { showErr('err-git-clone', 'URL is required'); return; }
 
  hideErr('err-git-clone');
  var btn = document.querySelector('#m-git-clone .btn-primary');
  btn.textContent = 'Cloning...'; btn.disabled = true;
 
  api('/api/git/clone', 'POST', { url: url, name: name || undefined }).then(function(d) {
    btn.textContent = 'Clone'; btn.disabled = false;
    if (!d.ok) { showErr('err-git-clone', d.error); return; }
    cm('m-git-clone');
    el('git-clone-url').value = ''; el('git-clone-name').value = '';
    navigate('git');
  });
}
 
// ── Commit ───────────────────────────────────────────────
function openGitCommit(repoName) {
  el('git-commit-repo').value = repoName;
  el('git-commit-title').textContent = 'Commit — ' + repoName;
  el('git-commit-msg').value = '';
  hideErr('err-git-commit');
  om('m-git-commit');
}
 
function submitGitCommit() {
  var repo    = val('git-commit-repo');
  var message = val('git-commit-msg').trim();
  var author  = val('git-commit-author').trim();
  if (!message) { showErr('err-git-commit', 'Commit message is required'); return; }
 
  hideErr('err-git-commit');
  var btn = document.querySelector('#m-git-commit .btn-primary');
  btn.textContent = 'Committing...'; btn.disabled = true;
 
  api('/api/git/commit/' + encodeURIComponent(repo), 'POST', { message: message, author: author || undefined }).then(function(d) {
    btn.textContent = 'Commit'; btn.disabled = false;
    if (!d.ok) { showErr('err-git-commit', d.error); return; }
    cm('m-git-commit');
    navigate('git');
  });
}
 
// ── Branch ───────────────────────────────────────────────
function openGitBranch(repoName) {
  el('git-branch-repo').value = repoName;
  el('git-branch-name').value = '';
  el('git-branch-from').value = '';
  hideErr('err-git-branch');
  om('m-git-branch');
}
 
function submitGitBranch() {
  var repo   = val('git-branch-repo');
  var branch = val('git-branch-name').trim();
  var from   = val('git-branch-from').trim();
  if (!branch) { showErr('err-git-branch', 'Branch name is required'); return; }
 
  hideErr('err-git-branch');
  var btn = document.querySelector('#m-git-branch .btn-primary');
  btn.textContent = 'Creating...'; btn.disabled = true;
 
  api('/api/git/branch/' + encodeURIComponent(repo), 'POST', { branch: branch, from: from || undefined }).then(function(d) {
    btn.textContent = 'Create'; btn.disabled = false;
    if (!d.ok) { showErr('err-git-branch', d.error); return; }
    cm('m-git-branch');
    navigate('git');
  });
}
 
// ── Pull ─────────────────────────────────────────────────
function gitPull(repoName, btn) {
  btn.textContent = 'Pulling...'; btn.disabled = true;
  api('/api/git/pull/' + encodeURIComponent(repoName), 'POST').then(function(d) {
    btn.textContent = '⬇ Pull'; btn.disabled = false;
    if (!d.ok) { alert('Pull failed: ' + d.error); return; }
    var msg = d.summary ? ('Changes: +' + d.summary.changes + ' insertions, -' + d.summary.deletions + ' deletions') : 'Already up to date.';
    alert('Pull complete — ' + repoName + '\n' + msg);
    navigate('git');
  });
}
 
// ── Remote ───────────────────────────────────────────────
function openGitRemote(repoName) {
  el('git-remote-repo').value = repoName;
  el('git-remote-url').value = '';
  el('git-remote-name').value = 'origin';
  hideErr('err-git-remote');
  om('m-git-remote');
}
 
function submitGitRemote() {
  var repo   = val('git-remote-repo');
  var name   = val('git-remote-name').trim() || 'origin';
  var url    = val('git-remote-url').trim();
  if (!url) { showErr('err-git-remote', 'URL is required'); return; }
 
  hideErr('err-git-remote');
  var btn = document.querySelector('#m-git-remote .btn-primary');
  btn.textContent = 'Adding...'; btn.disabled = true;
 
  api('/api/git/remote/' + encodeURIComponent(repo), 'POST', { remoteName: name, url: url }).then(function(d) {
    btn.textContent = 'Add Remote'; btn.disabled = false;
    if (!d.ok) { showErr('err-git-remote', d.error); return; }
    cm('m-git-remote');
    // Refresh detail panel if open
    if (curGitDetailRepo === repo) loadGitDetailTab(curGitDetailTab);
  });
}
 
// ── Delete repo ───────────────────────────────────────────
function deleteGitRepo(repoName) {
  if (!confirm('Delete repository "' + repoName + '"?\n\nAll files and history will be permanently deleted.')) return;
  api('/api/git/repo/' + encodeURIComponent(repoName), 'DELETE').then(function(d) {
    if (!d.ok) { alert('Error: ' + d.error); return; }
    navigate('git');
  });
}
 
// ── Detail modal ─────────────────────────────────────────
function openGitDetail(repoName) {
  curGitDetailRepo = repoName;
  curGitDetailTab  = 'log';
  el('git-detail-title').textContent = '🌿 ' + repoName;
 
  // Reset tabs
  document.querySelectorAll('.git-detail-tab').forEach(function(t) { t.classList.remove('active'); });
  el('tab-log').classList.add('active');
 
  om('m-git-detail');
  loadGitDetailTab('log');
}
 
function switchGitTab(tab, btnEl) {
  curGitDetailTab = tab;
  document.querySelectorAll('.git-detail-tab').forEach(function(t) { t.classList.remove('active'); });
  btnEl.classList.add('active');
  loadGitDetailTab(tab);
}
 
function loadGitDetailTab(tab) {
  var c = el('git-detail-content');
  c.innerHTML = '<div class="loading">Loading...</div>';
 
  if (tab === 'log') {
    api('/api/git/log/' + encodeURIComponent(curGitDetailRepo) + '?limit=20').then(function(d) {
      if (!d.ok) { c.innerHTML = '<div class="err-box">' + d.error + '</div>'; return; }
      if (d.commits.length === 0) { c.innerHTML = '<p style="color:var(--text-muted)">No commits yet</p>'; return; }
 
      var html = '<div style="display:flex;flex-direction:column;gap:0">';
      d.commits.forEach(function(cm, i) {
        html += '<div style="display:flex;gap:12px;padding:10px 0;' + (i > 0 ? 'border-top:1px solid var(--border)' : '') + '">' +
          '<div style="flex-shrink:0;width:8px;height:8px;background:#3b82f6;border-radius:50%;margin-top:6px"></div>' +
          '<div style="flex:1;min-width:0">' +
            '<div style="font-size:13px;font-weight:500">' + escHtml(cm.message) + '</div>' +
            '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' +
              escHtml(cm.author_name) + ' · ' + formatDate(cm.date) +
              ' &nbsp;<code style="background:var(--bg-secondary);padding:1px 5px;border-radius:3px;font-size:10px">' + (cm.hash || '').slice(0,7) + '</code>' +
            '</div>' +
          '</div>' +
        '</div>';
      });
      c.innerHTML = html + '</div>';
    });
 
  } else if (tab === 'branches') {
    api('/api/git/branches/' + encodeURIComponent(curGitDetailRepo)).then(function(d) {
      if (!d.ok) { c.innerHTML = '<div class="err-box">' + d.error + '</div>'; return; }
 
      var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:12px">' +
        '<button class="btn-ghost" style="font-size:12px" onclick="openGitBranch(\'' + escAttr(curGitDetailRepo) + '\')">+ New Branch</button>' +
      '</div>';
 
      html += '<div style="display:flex;flex-direction:column;gap:6px">';
      d.all.forEach(function(b) {
        var isCurrent = b === d.current;
        html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:var(--bg-secondary);border-radius:6px">' +
          '<span style="font-family:monospace;font-size:13px">' +
            (isCurrent ? '→ ' : '&nbsp;&nbsp; ') + escHtml(b) +
            (isCurrent ? ' <span class="badge badge-green" style="margin-left:6px">current</span>' : '') +
          '</span>' +
          (!isCurrent
            ? '<button class="btn-ghost" style="font-size:11px" onclick="gitCheckout(\'' + escAttr(curGitDetailRepo) + '\',\'' + escAttr(b) + '\')">Checkout</button>'
            : '') +
        '</div>';
      });
      c.innerHTML = html + '</div>';
    });
 
  } else if (tab === 'status') {
    api('/api/git/status/' + encodeURIComponent(curGitDetailRepo)).then(function(d) {
      if (!d.ok) { c.innerHTML = '<div class="err-box">' + d.error + '</div>'; return; }
      var s = d.status;
      var html = '<div style="display:flex;gap:16px;margin-bottom:16px">' +
        '<div class="stat-box"><div class="stat-val">' + s.files.length + '</div><div class="stat-label">Changed Files</div></div>' +
        '<div class="stat-box"><div class="stat-val" style="color:' + (s.isClean() ? '#22c55e' : '#f59e0b') + '">' + (s.isClean() ? 'Clean' : 'Dirty') + '</div><div class="stat-label">Status</div></div>' +
      '</div>';
 
      if (s.files.length > 0) {
        html += '<div style="display:flex;justify-content:flex-end;margin-bottom:10px">' +
          '<button class="btn-primary" style="font-size:12px" onclick="cm(\'m-git-detail\');openGitCommit(\'' + escAttr(curGitDetailRepo) + '\')">✅ Commit All Changes</button>' +
        '</div>';
        html += '<table class="data-table"><thead><tr><th>File</th><th>State</th></tr></thead><tbody>';
        s.files.forEach(function(f) {
          var stateColor = f.index === '?' ? '#94a3b8' : (f.index === 'M' || f.working_dir === 'M') ? '#f59e0b' : '#22c55e';
          html += '<tr>' +
            '<td style="font-family:monospace;font-size:12px">' + escHtml(f.path) + '</td>' +
            '<td><span style="color:' + stateColor + ';font-size:12px">' + escHtml(f.index || f.working_dir) + '</span></td>' +
          '</tr>';
        });
        html += '</tbody></table>';
      } else {
        html += '<p style="color:#22c55e;text-align:center;padding:20px">✓ Working tree clean</p>';
      }
      c.innerHTML = html;
    });
 
  } else if (tab === 'remotes') {
    api('/api/git/remotes/' + encodeURIComponent(curGitDetailRepo)).then(function(d) {
      if (!d.ok) { c.innerHTML = '<div class="err-box">' + d.error + '</div>'; return; }
 
      var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:12px">' +
        '<button class="btn-ghost" style="font-size:12px" onclick="openGitRemote(\'' + escAttr(curGitDetailRepo) + '\')">+ Add Remote</button>' +
      '</div>';
 
      if (d.remotes.length === 0) {
        html += '<p style="color:var(--text-muted);text-align:center;padding:20px">No remotes configured</p>';
      } else {
        html += '<table class="data-table"><thead><tr><th>Name</th><th>Fetch URL</th><th>Push URL</th></tr></thead><tbody>';
        d.remotes.forEach(function(r) {
          html += '<tr>' +
            '<td style="font-weight:500">' + escHtml(r.name) + '</td>' +
            '<td style="font-family:monospace;font-size:11px">' + escHtml(r.refs && r.refs.fetch || '') + '</td>' +
            '<td style="font-family:monospace;font-size:11px">' + escHtml(r.refs && r.refs.push || '') + '</td>' +
          '</tr>';
        });
        html += '</tbody></table>';
      }
      c.innerHTML = html;
    });
  }
}
 
function gitCheckout(repoName, branch) {
  api('/api/git/checkout/' + encodeURIComponent(repoName), 'POST', { branch: branch }).then(function(d) {
    if (!d.ok) { alert('Checkout failed: ' + d.error); return; }
    loadGitDetailTab('branches');
    navigate('git'); // refresh cards
  });
}
 
 
//<!-- ═══════════════════════════════════════════════════════════════
     //STEP 6 — Add to the navigate() / renderPage() switch-case
     //Find the switch(page) block and add these two cases:
//════════════════════════════════════════════════════════════════ -->
 
//<!-- case 'backup': loadBackup(content); break; case 'git':    loadGit(content);    break; -->
 
function installNpmPackages(packages, pageName) {
  var btn = el('npm-install-btn');
  var statusEl = el('npm-install-status');
  if (btn) { btn.textContent = 'Installing...'; btn.disabled = true; }
  if (statusEl) { statusEl.textContent = 'Running npm install ' + packages.join(' ') + '...'; statusEl.style.color = '#64748b'; }

  api('/api/system/npm-install', 'POST', { packages: packages }).then(function(r) {
    if (!r.ok) {
      if (btn) { btn.textContent = 'Install Dependencies'; btn.disabled = false; }
      if (statusEl) { statusEl.textContent = '✗ Failed: ' + r.error; statusEl.style.color = '#dc2626'; }
      return;
    }
    if (statusEl) { statusEl.textContent = '✓ Packages installed! Restarting server...'; statusEl.style.color = '#16a34a'; }
    if (btn) { btn.textContent = 'Restarting...'; btn.disabled = true; }

    // Restart the server
    api('/api/system/restart', 'POST').then(function() {
      if (statusEl) { statusEl.textContent = '✓ Server restarting — loading page in 3 seconds...'; }
      setTimeout(function() {
        showPage(pageName, null);
      }, 3000);
    }).catch(function() {
      // Server restarted so request failed — that's expected, just wait and reload
      if (statusEl) { statusEl.textContent = '✓ Server restarting — loading page in 3 seconds...'; }
      setTimeout(function() {
        showPage(pageName, null);
      }, 3000);
    });
  }).catch(function(e) {
    if (btn) { btn.textContent = 'Install Dependencies'; btn.disabled = false; }
    if (statusEl) { statusEl.textContent = '✗ Error: ' + e.message; statusEl.style.color = '#dc2626'; }
  });
}

function missingModuleBanner(packages, pageName) {
  var pkgStr = packages.join(' ');
  var pkgArr = "'" + packages.join("','") + "'";
  return '<div class="card" style="max-width:520px;margin:40px auto;text-align:center;padding:32px">' +
    '<div style="font-size:40px;margin-bottom:12px">📦</div>' +
    '<h3 style="margin-bottom:8px">Missing Dependencies</h3>' +
    '<p style="font-size:13px;color:var(--text-muted);margin-bottom:6px">' +
      'The following npm packages are required but not installed:' +
    '</p>' +
    '<p style="font-family:monospace;font-size:13px;background:var(--bg-secondary);padding:8px 14px;border-radius:6px;margin-bottom:20px;display:inline-block">' +
      pkgStr +
    '</p><br>' +
    '<button id="npm-install-btn" class="btn-primary" ' +
      'onclick="installNpmPackages([' + pkgArr + '],\'' + pageName + '\')" ' +
      'style="margin-bottom:10px">📦 Install Dependencies</button>' +
    '<br><span id="npm-install-status" style="font-size:12px"></span>' +
    '<p style="font-size:11px;color:var(--text-muted);margin-top:14px">' +
      'Or run manually: <code>cd D:\\panel &amp;&amp; npm install ' + pkgStr + '</code>' +
    '</p>' +
  '</div>';
}

function checkServerRestart() {
  api('/api/ping').then(function(r) {
    if (!r || !r.started) return;
    if (_serverStart !== null && r.started !== _serverStart) {
      // Server was restarted — wipe all cached data
      Object.keys(_cache).forEach(function(k) { cacheClear(k); });
      console.log('[cache] Server restart detected — all cache cleared');
    }
    _serverStart = r.started;
  }).catch(function() {
    // Backend unreachable — clear cache to be safe
    Object.keys(_cache).forEach(function(k) { cacheClear(k); });
  });
}

// ── Server restart detection — runs every 15 seconds ───────
function startRestartDetection() {
  // Run immediately on startup
  checkServerRestart();

  // Then check every 15 seconds in background
  setInterval(function() {
    checkServerRestart();
  }, 15000);
}

function saveSMTP() {
  var data = {
    host:    val('sm-host'),
    port:    parseInt(val('sm-port')) || 587,
    user:    val('sm-user'),
    pass:    el('sm-pass').value,
    from:    val('sm-from'),
    secure:  el('sm-secure').checked
  };
  var btn = document.querySelector('#sm-saved');
  api('/api/smtp', 'POST', data).then(function(r) {
    if (r.ok) {
      cacheClear('smtp');
      var s = el('sm-saved');
      if (s) { s.style.display = 'inline'; setTimeout(function(){ s.style.display = 'none'; }, 3000); }
    } else {
      alert('Save failed: ' + (r.error || 'Unknown error'));
    }
  }).catch(function(e) { alert('Error: ' + e.message); });
}

function testSMTP() {
  var to = val('sm-test-to').trim();
  if (!to) { alert('Enter a recipient email address'); return; }
  var result = el('sm-test-result');
  result.innerHTML = '<span style="color:#64748b">Sending...</span>';
  api('/api/smtp/test', 'POST', { to: to }).then(function(r) {
    if (r.ok) result.innerHTML = '<span style="color:#16a34a">✓ Test email sent successfully</span>';
    else      result.innerHTML = '<span style="color:#dc2626">✗ Failed: ' + (r.error || 'Unknown error') + '</span>';
  }).catch(function(e) { result.innerHTML = '<span style="color:#dc2626">Error: ' + e.message + '</span>'; });
}

// ═══════════════════════════════════════════════════════════
// REDIRECTS PAGE — paste into appData.js before if(TOKEN){initApp();}
// ═══════════════════════════════════════════════════════════

var _redirectEditId = null;

function loadRedirects(c) {
  // Skeleton immediately
  c.innerHTML =
    '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
      '<div class="sk-line" style="width:200px;height:13px"></div>' +
      '<div style="display:flex;gap:8px">' +
        '<div class="sk-line" style="width:80px;height:32px;border-radius:6px"></div>' +
        '<div class="sk-line" style="width:110px;height:32px;border-radius:6px"></div>' +
      '</div>' +
    '</div>' +
    skeletonCard('Redirects', ['Source','Destination','Type','Status','Hits','Actions'], 5);

  var cached = cacheGet('redirects');
  if (cached) { renderRedirectsList(c, cached); return; }

  api('/api/redirects').then(function(d) {
    if (!d.ok) { c.innerHTML = '<div class="err-box">' + d.error + '</div>'; return; }
    cacheSet('redirects', d.redirects);
    renderRedirectsList(c, d.redirects);
  }).catch(function() { c.innerHTML = '<div class="err-box">Could not load redirects.</div>'; });
}

function renderRedirectsList(c, list) {
  var activeCount   = list.filter(function(r) { return r.active; }).length;
  var inactiveCount = list.length - activeCount;
  var totalHits     = list.reduce(function(s, r) { return s + (r.hits || 0); }, 0);

  var html =
    '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">' +
      '<div style="display:flex;gap:16px">' +
        '<div style="font-size:13px;color:var(--text-muted)">' +
          '<b style="color:var(--text)">' + list.length + '</b> total &nbsp;|&nbsp; ' +
          '<b style="color:#16a34a">' + activeCount + '</b> active &nbsp;|&nbsp; ' +
          '<b style="color:#94a3b8">' + inactiveCount + '</b> inactive &nbsp;|&nbsp; ' +
          '<b style="color:#3b82f6">' + totalHits + '</b> total hits' +
        '</div>' +
      '</div>' +
      '<div style="display:flex;gap:8px">' +
        '<button class="btn-neutral" onclick="cacheClear(\'redirects\');loadRedirects(el(\'content\'))">&#8635; Refresh</button>' +
        '<button class="btn-primary" onclick="openAddRedirect()">+ Add Redirect</button>' +
      '</div>' +
    '</div>';

  // Test bar
  html +=
    '<div class="card" style="margin-bottom:16px;padding:12px">' +
      '<div style="display:flex;gap:8px;align-items:center">' +
        '<span style="font-size:13px;color:var(--text-muted);white-space:nowrap">Test path:</span>' +
        '<input id="rd-test-input" placeholder="/old-page" style="flex:1;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;outline:none" ' +
          'onkeydown="if(event.key===\'Enter\')testRedirectPath()" />' +
        '<button class="btn-neutral" onclick="testRedirectPath()">Test</button>' +
        '<span id="rd-test-result" style="font-size:12px;min-width:200px"></span>' +
      '</div>' +
    '</div>';

  if (list.length === 0) {
    html +=
      '<div class="empty-state" style="text-align:center;padding:60px 20px">' +
        '<div style="font-size:48px;margin-bottom:12px">↪</div>' +
        '<h3 style="margin-bottom:8px">No redirects yet</h3>' +
        '<p style="font-size:13px;color:var(--text-muted);margin-bottom:20px">Add a redirect to forward one URL to another</p>' +
        '<button class="btn-primary" onclick="openAddRedirect()">+ Add First Redirect</button>' +
      '</div>';
  } else {
    html += '<div class="card"><table><thead><tr>' +
      '<th>Source Path</th><th>Destination</th><th>Type</th><th>Status</th><th style="text-align:right">Hits</th><th>Note</th><th style="text-align:right">Actions</th>' +
      '</tr></thead><tbody>';

    list.forEach(function(r) {
      var typeColor  = r.type === 301 ? '#7c3aed' : '#0369a1';
      var typeBg     = r.type === 301 ? '#f5f3ff' : '#e0f2fe';
      var safeId     = r.id.replace(/'/g, "\\'");
      var safeSrc    = escHtml(r.source);
      var safeDest   = escHtml(r.destination);
      var safeNote   = escHtml(r.note || '');

      html += '<tr style="' + (!r.active ? 'opacity:0.5' : '') + '">' +
        '<td style="font-family:monospace;font-size:12px;font-weight:500">' + safeSrc + '</td>' +
        '<td style="font-size:12px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + safeDest + '">' +
          '<a href="' + safeDest + '" target="_blank" style="color:#3b82f6;text-decoration:none">' + safeDest + '</a>' +
        '</td>' +
        '<td>' +
          '<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px;background:' + typeBg + ';color:' + typeColor + '">' +
            r.type + (r.type === 301 ? ' Permanent' : ' Temporary') +
          '</span>' +
        '</td>' +
        '<td>' +
          '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin:0">' +
            '<input type="checkbox" ' + (r.active ? 'checked' : '') + ' onchange="toggleRedirect(\'' + safeId + '\',this)" style="width:auto;cursor:pointer" />' +
            '<span style="font-size:12px;color:' + (r.active ? '#16a34a' : '#94a3b8') + '">' + (r.active ? 'Active' : 'Disabled') + '</span>' +
          '</label>' +
        '</td>' +
        '<td style="text-align:right;font-size:12px;color:#64748b">' + (r.hits || 0) + '</td>' +
        '<td style="font-size:12px;color:#94a3b8;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + safeNote + '">' + (safeNote || '—') + '</td>' +
        '<td style="text-align:right;white-space:nowrap">' +
          '<button class="btn-neutral" style="font-size:11px" onclick="openEditRedirect(\'' + safeId + '\')">Edit</button>' +
          '<button class="btn-danger"  style="font-size:11px" onclick="deleteRedirect(\'' + safeId + '\')">Delete</button>' +
        '</td>' +
      '</tr>';
    });

    html += '</tbody></table></div>';
  }

  c.innerHTML = html;
}

// ── Test a path ───────────────────────────────────────────
function testRedirectPath() {
  var source = (el('rd-test-input') || {}).value || '';
  var result = el('rd-test-result');
  if (!source) { if (result) { result.textContent = 'Enter a path to test'; result.style.color = '#94a3b8'; } return; }
  if (result) { result.textContent = 'Testing...'; result.style.color = '#64748b'; }

  api('/api/redirects/test?source=' + encodeURIComponent(source)).then(function(d) {
    if (!d.ok) { result.textContent = '✗ Error: ' + d.error; result.style.color = '#dc2626'; return; }
    if (d.found) {
      result.innerHTML = '✓ Redirects to <b>' + escHtml(d.redirect.destination) + '</b> (' + d.redirect.type + ')';
      result.style.color = '#16a34a';
    } else {
      result.textContent = '✗ No active redirect found';
      result.style.color = '#f59e0b';
    }
  }).catch(function() { result.textContent = '✗ Could not test'; result.style.color = '#dc2626'; });
}

// ── Toggle active ─────────────────────────────────────────
function toggleRedirect(id, checkbox) {
  api('/api/redirects/' + id + '/toggle', 'POST').then(function(d) {
    if (!d.ok) { alert('Error: ' + d.error); checkbox.checked = !checkbox.checked; return; }
    var span = checkbox.nextElementSibling;
    if (span) { span.textContent = d.active ? 'Active' : 'Disabled'; span.style.color = d.active ? '#16a34a' : '#94a3b8'; }
    var row = checkbox.closest('tr');
    if (row) row.style.opacity = d.active ? '1' : '0.5';
    cacheClear('redirects');
  }).catch(function() { checkbox.checked = !checkbox.checked; });
}

// ── Add redirect ──────────────────────────────────────────
function openAddRedirect() {
  _redirectEditId = null;
  el('rd-modal-title').textContent  = 'Add Redirect';
  el('rd-source').value             = '';
  el('rd-destination').value        = '';
  el('rd-type').value               = '301';
  el('rd-note').value               = '';
  el('rd-active').checked           = true;
  hideErr('rd-err');
  om('m-redirect');
}

// ── Edit redirect ─────────────────────────────────────────
function openEditRedirect(id) {
  var cached = cacheGet('redirects');
  var list   = cached || [];
  var r      = list.find(function(r) { return r.id === id; });
  if (!r) { alert('Redirect not found — please refresh'); return; }

  _redirectEditId                   = id;
  el('rd-modal-title').textContent  = 'Edit Redirect';
  el('rd-source').value             = r.source;
  el('rd-destination').value        = r.destination;
  el('rd-type').value               = String(r.type);
  el('rd-note').value               = r.note || '';
  el('rd-active').checked           = r.active;
  hideErr('rd-err');
  om('m-redirect');
}

// ── Submit add/edit ───────────────────────────────────────
function submitRedirect() {
  var source      = val('rd-source').trim();
  var destination = val('rd-destination').trim();
  var type        = val('rd-type');
  var note        = val('rd-note').trim();
  var active      = el('rd-active').checked;

  if (!source)      { showErr('rd-err', 'Source path is required'); return; }
  if (!source.startsWith('/')) { showErr('rd-err', 'Source must start with / e.g. /old-page'); return; }
  if (!destination) { showErr('rd-err', 'Destination is required'); return; }

  hideErr('rd-err');
  var btn = document.querySelector('#m-redirect .btn-primary');
  btn.textContent = 'Saving...'; btn.disabled = true;

  var isEdit  = !!_redirectEditId;
  var url     = '/api/redirects' + (isEdit ? '/' + _redirectEditId : '');
  var method  = isEdit ? 'PUT' : 'POST';

  api(url, method, { source: source, destination: destination, type: type, note: note, active: active })
    .then(function(d) {
      btn.textContent = 'Save'; btn.disabled = false;
      if (!d.ok) { showErr('rd-err', d.error); return; }
      cm('m-redirect');
      cacheClear('redirects');
      loadRedirects(el('content'));
    })
    .catch(function(e) { btn.textContent = 'Save'; btn.disabled = false; showErr('rd-err', e.message); });
}

// ── Delete redirect ───────────────────────────────────────
function deleteRedirect(id) {
  if (!confirm('Delete this redirect? This cannot be undone.')) return;
  api('/api/redirects/' + id, 'DELETE').then(function(d) {
    if (!d.ok) { alert('Error: ' + d.error); return; }
    cacheClear('redirects');
    loadRedirects(el('content'));
  }).catch(function(e) { alert('Error: ' + e.message); });
}


if (TOKEN) { initApp(); }