Newer
Older
navi-1 / admin / index.html
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 9 May 34 KB Add profile editing to admin panel
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Navi — Admin</title>
  <link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.0.3/src/regular/style.css">
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    :root {
      --bg: #0e0e0e; --bg2: #161616; --bg3: #1e1e1e; --bg4: #222;
      --border: #252525; --border2: #2e2e2e;
      --text: #d4d4d4; --text2: #888; --text3: #555;
      --accent: #4ec9b0;
      --danger: #f48771;
    }
    html, body { height: 100%; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      font-size: 13px;
      background: var(--bg);
      color: var(--text);
      display: flex;
      flex-direction: column;
      height: 100vh;
      overflow: hidden;
    }
    header {
      flex-shrink: 0;
      background: var(--bg2);
      border-bottom: 1px solid var(--border);
      padding: 0 14px;
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .logo {
      font-size: 11px; color: var(--text3); letter-spacing: .1em;
      text-transform: uppercase; white-space: nowrap;
      align-self: center; padding: 10px 0;
    }
    .logo b { color: var(--accent); }
    .tabs {
      display: flex; align-items: stretch; gap: 2px; margin-left: 10px;
    }
    .tab {
      padding: 0 14px; font-family: inherit; font-size: 12px;
      color: var(--text3); background: none; border: none;
      border-bottom: 2px solid transparent; cursor: pointer;
      transition: color .15s, border-color .15s; line-height: 40px;
    }
    .tab:hover { color: var(--text2); }
    .tab.active { color: var(--text); border-bottom-color: var(--accent); }
    .header-right {
      margin-left: auto;
      display: flex; align-items: center; gap: 12px;
    }
    .header-right a {
      color: var(--text2); text-decoration: none; font-size: 12px;
    }
    .header-right a:hover { color: var(--text); }
    main { flex: 1; overflow-y: auto; padding: 14px 16px; }
    .pane { display: none; }
    .pane.active { display: block; }

    /* Tables */
    table {
      width: 100%; border-collapse: collapse; font-size: 13px;
    }
    th, td {
      padding: 8px 12px; border-bottom: 1px solid var(--border);
      text-align: left; color: var(--text);
    }
    th {
      background: var(--bg2); font-weight: 600; color: #fff;
      position: sticky; top: 0; z-index: 1;
    }
    tr:hover td { background: var(--bg2); }
    .mono { font-family: monospace; font-size: 11px; color: var(--text2); }
    .dim { color: var(--text3); }
    .right { text-align: right; }

    /* Controls */
    input, select, button {
      font-family: inherit; font-size: 12px;
      background: var(--bg3); border: 1px solid var(--border2);
      color: var(--text); border-radius: 4px; outline: none;
    }
    input, select { padding: 6px 10px; }
    button { padding: 6px 12px; cursor: pointer; }
    button:hover { background: var(--bg4); border-color: #555; }
    button.danger { background: #3a1e1e; border-color: #522; }
    button.danger:hover { background: #522; }
    .toolbar {
      display: flex; align-items: center; gap: 12px;
      margin-bottom: 12px; flex-wrap: wrap;
    }
    .search-input {
      flex: 1; max-width: 320px;
      background: var(--bg3); color: var(--text);
      border: 1px solid var(--border2); padding: 6px 10px;
    }

    /* Switch */
    .switch {
      position: relative; display: inline-block;
      width: 40px; height: 22px;
    }
    .switch input { opacity: 0; width: 0; height: 0; }
    .slider {
      position: absolute; cursor: pointer;
      top: 0; left: 0; right: 0; bottom: 0;
      background: #444; transition: .2s; border-radius: 22px;
    }
    .slider:before {
      position: absolute; content: "";
      height: 16px; width: 16px; left: 3px; bottom: 3px;
      background: white; transition: .2s; border-radius: 50%;
    }
    input:checked + .slider { background: var(--accent); }
    input:checked + .slider:before { transform: translateX(18px); }

    /* Drawer */
    .drawer-overlay {
      position: fixed; inset: 0;
      background: rgba(0,0,0,0.5); z-index: 200;
      display: flex; justify-content: flex-end;
    }
    .drawer {
      width: 500px; background: var(--bg2);
      padding: 1rem; overflow: auto;
      border-left: 1px solid var(--border);
    }
    .drawer pre {
      font-size: 12px; color: var(--text);
      white-space: pre-wrap; word-break: break-word;
    }
    .drawer h3 { margin-bottom: 10px; color: #fff; }

    /* Group */
    .group-title {
      margin: 12px 0 8px; color: #fff; font-size: 14px;
      text-transform: capitalize;
    }

    /* Form fields inside drawer */
    #profile-form label {
      display: block;
      color: var(--text2);
      font-size: 12px;
      margin: 8px 0 4px;
    }
    #profile-form input[type="text"],
    #profile-form input[type="number"],
    #profile-form select,
    #profile-form textarea {
      width: 100%;
      background: var(--bg3);
      border: 1px solid var(--border2);
      color: var(--text);
      padding: 6px 10px;
      font-size: 13px;
      border-radius: 4px;
      font-family: inherit;
    }
    #profile-form input[type="checkbox"] {
      margin-right: 6px;
      accent-color: var(--accent);
    }
    #profile-form textarea {
      resize: vertical;
    }
    #profile-form section {
      border-bottom: 1px solid var(--border);
      padding-bottom: 8px;
    }
    .value-cell {
      max-width: 400px; overflow: hidden;
      text-overflow: ellipsis; white-space: nowrap;
    }

    .placeholder {
      text-align: center; color: var(--text3);
      padding: 80px 0; font-size: 13px;
    }
    .error-msg { color: var(--danger); padding: 12px; }

    /* Sortable headers */
    th.sortable { cursor: pointer; user-select: none; }
    th.sortable::after { content: ' ⇅'; color: var(--text3); font-size: 10px; }
    th.sortable.sort-asc::after { content: ' ▲'; color: var(--accent); }
    th.sortable.sort-desc::after { content: ' ▼'; color: var(--accent); }

    /* Pagination */
    .pagination-btn {
      padding: 4px 10px; font-size: 12px; min-width: 32px;
      background: var(--bg3); border: 1px solid var(--border2);
      color: var(--text); border-radius: 4px; cursor: pointer;
    }
    .pagination-btn:hover:not(:disabled) { background: var(--bg4); }
    .pagination-btn:disabled { opacity: 0.4; cursor: not-allowed; }
    .pagination-info { color: var(--text2); font-size: 12px; margin: 0 8px; }

    /* Scrollbar */
    ::-webkit-scrollbar { width: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
    ::-webkit-scrollbar-thumb:hover { background: #444; }
  </style>
</head>
<body>

<header>
  <div class="logo"><b>NAVI</b> ADMIN</div>
  <div class="tabs">
    <button class="tab active" data-tab="users">Users</button>
    <button class="tab" data-tab="sessions">Sessions</button>
    <button class="tab" data-tab="memory">Memory</button>
    <button class="tab" data-tab="profiles">Profiles</button>
  </div>
  <div class="header-right">
    <a href="/debug">Debug</a>
    <a href="/debug/eval">Eval</a>
    <a href="/">App</a>
  </div>
</header>

<main>
  <!-- Users -->
  <div id="pane-users" class="pane active">
    <div id="users-content"><div class="placeholder">Loading users…</div></div>
  </div>

  <!-- Sessions -->
  <div id="pane-sessions" class="pane">
    <div class="toolbar">
      <input id="sess-search" class="search-input" placeholder="Search session ID, name, user or profile…">
      <span id="sess-count" class="dim"></span>
    </div>
    <div id="sessions-content"><div class="placeholder">Loading sessions…</div></div>
    <div id="sess-pagination" class="toolbar" style="justify-content:center; margin-top:8px;"></div>
  </div>

  <!-- Memory -->
  <div id="pane-memory" class="pane">
    <div class="toolbar">
      <input id="mem-search" class="search-input" placeholder="Search key, value or category…">
      <span id="mem-count" class="dim"></span>
    </div>
    <div id="memory-content"><div class="placeholder">Loading memory…</div></div>
    <div id="mem-pagination" class="toolbar" style="justify-content:center; margin-top:8px;"></div>
  </div>

  <!-- Profiles -->
  <div id="pane-profiles" class="pane">
    <div id="profiles-content"><div class="placeholder">Loading profiles…</div></div>
  </div>
</main>

<script>
'use strict';

// ── tiny utils ──────────────────────────────────────────────────────────
const $ = (sel) => document.querySelector(sel);
const fmtDate = (d) => d ? new Date(d).toLocaleString() : '—';
const esc = (s) => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');

const api = {
  async request(method, path, body) {
    const opts = { method, credentials: 'include', headers: {} };
    if (body !== undefined) {
      opts.headers['Content-Type'] = 'application/json';
      opts.body = JSON.stringify(body);
    }
    try {
      const r = await fetch(path, opts);
      if (r.status === 401) {
        window.location.href = '/';
        return;
      }
      if (!r.ok) {
        const txt = await r.text().catch(() => '');
        throw new Error(`${method} ${path} → ${r.status}: ${txt}`);
      }
      if (r.status === 204) return null;
      return r.json();
    } catch (err) {
      if (err.name === 'TypeError') {
        throw new Error('Network error — check your connection or server availability');
      }
      throw err;
    }
  },
  get(path) { return this.request('GET', path); },
  patch(path, body) { return this.request('PATCH', path, body); },
  del(path) { return this.request('DELETE', path); },

  users()    { return this.get('/admin/users'); },
  sessions(qs='') { return this.get('/admin/sessions' + (qs ? '?' + qs : '')); },
  memory(qs='') { return this.get('/admin/memory' + (qs ? '?' + qs : '')); },
  profiles() { return this.get('/admin/profiles'); },
  profile(id) { return this.get(`/admin/profiles/${id}`); },
  saveProfile(id, body) { return this.request('PUT', `/admin/profiles/${id}`, body); },
  session(id){ return this.get(`/admin/sessions/${id}`); },
  delSession(id){ return this.del(`/admin/sessions/${id}`); },
  userSessions(id){ return this.get(`/admin/users/${id}/sessions`); },
  updateRole(id, role){ return this.patch(`/admin/users/${id}/role`, { role }); },
  updateProfile(id, isAdminOnly){ return this.patch(`/admin/profiles/${id}/availability`, { is_admin_only: isAdminOnly }); },
};

// ── tabs ─────────────────────────────────────────────────────────────────
function switchTab(tab) {
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
  document.querySelectorAll('.pane').forEach(p => p.classList.toggle('active', p.id === `pane-${tab}`));
  history.replaceState(null, '', `#${tab}`);
  if (tab === 'users' && !state.usersLoaded) loadUsers();
  if (tab === 'sessions' && !state.sessionsLoaded) loadSessions();
  if (tab === 'memory' && !state.memoryLoaded) loadMemory();
  if (tab === 'profiles' && !state.profilesLoaded) loadProfiles();
}

document.querySelectorAll('.tab').forEach(btn =>
  btn.addEventListener('click', () => {
    if (btn.dataset.tab === 'sessions') { sessState.search = ''; $('#sess-search').value = ''; }
    if (btn.dataset.tab === 'memory') { memState.userId = null; }
    switchTab(btn.dataset.tab);
  })
);

const state = { usersLoaded: false, sessionsLoaded: false, memoryLoaded: false, profilesLoaded: false };

// ── Users ────────────────────────────────────────────────────────────────
async function loadUsers() {
  const el = $('#users-content');
  try {
    const users = await api.users();
    state.usersLoaded = true;
    el.innerHTML = renderUsers(users);
  } catch (err) {
    el.innerHTML = `<div class="error-msg">${esc(err.message)}</div>`;
  }
}

function renderUsers(users) {
  if (!users.length) return '<div class="placeholder">No users.</div>';
  let html = '<table><thead><tr>';
  ['ID','Email','Display Name','Role','Permissions','Created','Actions'].forEach(h => html += `<th>${h}</th>`);
  html += '</tr></thead><tbody>';
  for (const u of users) {
    const perms = (() => { try { const a = JSON.parse(u.permissions); return Array.isArray(a) && a.length ? a.join(', ') : '—'; } catch { return '—'; } })();
    html += `<tr>
      <td class="mono">${esc(u.id)}</td>
      <td>${esc(u.email)}</td>
      <td>${esc(u.display_name || '—')}</td>
      <td><select onchange="onRoleChange('${esc(u.id)}', this.value)">
        <option value="user" ${u.role==='user'?'selected':''}>user</option>
        <option value="admin" ${u.role==='admin'?'selected':''}>admin</option>
      </select></td>
      <td>${esc(perms)}</td>
      <td>${fmtDate(u.created_at)}</td>
      <td>
        <button onclick="showUserSessions('${esc(u.id)}')">Sessions</button>
        <button onclick="showUserMemory('${esc(u.id)}')">Memory</button>
        <button onclick="showUserDetail('${esc(u.id)}')">Detail</button>
      </td>
    </tr>`;
  }
  html += '</tbody></table>';
  return html;
}

window.onRoleChange = async (id, role) => {
  try { await api.updateRole(id, role); } catch (err) { alert(err.message); }
};
window.showUserDetail = async (id) => {
  try {
    const u = (await api.users()).find(x => x.id === id);
    showDrawer('User Detail', JSON.stringify(u, null, 2));
  } catch (err) { alert(err.message); }
};
window.showUserSessions = (id) => {
  sessState.search = id;
  $('#sess-search').value = id;
  switchTab('sessions');
};
window.showUserMemory = (id) => {
  memState.userId = id;
  switchTab('memory');
};

// ── Sessions ───────────────────────────────────────────────────────────────
const sessState = { limit: 50, offset: 0, search: '', sortBy: 'last_active', sortOrder: 'desc', total: 0, items: [] };

async function loadSessions() {
  const el = $('#sessions-content');
  const countEl = $('#sess-count');
  try {
    const params = new URLSearchParams();
    params.set('limit', String(sessState.limit));
    params.set('offset', String(sessState.offset));
    if (sessState.search) params.set('search', sessState.search);
    params.set('sort_by', sessState.sortBy);
    params.set('sort_order', sessState.sortOrder);
    const data = await api.sessions(params.toString());
    state.sessionsLoaded = true;
    sessState.total = data.total || 0;
    sessState.items = data.items || [];
    el.innerHTML = renderSessionsTable(sessState.items);
    countEl.textContent = `${sessState.total} total`;
    renderSessionPagination();
  } catch (err) {
    el.innerHTML = `<div class="error-msg">${esc(err.message)}</div>`;
    countEl.textContent = '';
  }
}

const SESSION_COLUMNS = [
  { key: 'session_id', label: 'Session ID' },
  { key: 'user_id', label: 'User' },
  { key: 'profile_id', label: 'Profile' },
  { key: 'name', label: 'Name' },
  { key: 'message_count', label: 'Messages' },
  { key: 'pinned', label: 'Pinned' },
  { key: 'last_active', label: 'Last Active' },
  { key: 'actions', label: 'Actions', sortable: false },
];

function renderSessionsTable(items) {
  if (!items.length) return '<div class="placeholder">No sessions.</div>';
  let html = '<table><thead><tr>';
  for (const col of SESSION_COLUMNS) {
    const isSorted = sessState.sortBy === col.key;
    const cls = col.sortable !== false ? `sortable ${isSorted ? 'sort-' + sessState.sortOrder : ''}` : '';
    html += `<th class="${cls}" onclick="${col.sortable !== false ? `onSessionSort('${esc(col.key)}')` : ''}">${esc(col.label)}</th>`;
  }
  html += '</tr></thead><tbody>';
  for (const s of items) {
    html += `<tr>
      <td class="mono">${esc(s.session_id)}</td>
      <td class="mono">${esc(s.user_id || '—')}</td>
      <td>${esc(s.profile_id)}</td>
      <td>${esc(s.name || '—')}</td>
      <td class="right">${s.message_count}</td>
      <td>${s.pinned ? 'Yes' : 'No'}</td>
      <td>${fmtDate(s.last_active)}</td>
      <td>
        <button onclick="showSessionDetail('${esc(s.session_id)}')">Detail</button>
        <button class="danger" onclick="deleteSession('${esc(s.session_id)}')">Delete</button>
      </td>
    </tr>`;
  }
  html += '</tbody></table>';
  return html;
}

function renderSessionPagination() {
  const el = $('#sess-pagination');
  const start = sessState.total ? sessState.offset + 1 : 0;
  const end = Math.min(sessState.offset + sessState.limit, sessState.total);
  const hasPrev = sessState.offset > 0;
  const hasNext = sessState.offset + sessState.limit < sessState.total;
  el.innerHTML = `
    <button class="pagination-btn" onclick="changeSessionPage(-1)" ${hasPrev ? '' : 'disabled'}>Prev</button>
    <span class="pagination-info">${start}–${end} of ${sessState.total}</span>
    <button class="pagination-btn" onclick="changeSessionPage(1)" ${hasNext ? '' : 'disabled'}>Next</button>
    <select class="pagination-btn" onchange="setSessionLimit(this.value)" style="padding:4px 6px; min-width:60px;">
      <option value="10" ${sessState.limit===10?'selected':''}>10</option>
      <option value="25" ${sessState.limit===25?'selected':''}>25</option>
      <option value="50" ${sessState.limit===50?'selected':''}>50</option>
      <option value="100" ${sessState.limit===100?'selected':''}>100</option>
    </select>
  `;
}

window.changeSessionPage = (delta) => {
  sessState.offset = Math.max(0, sessState.offset + delta * sessState.limit);
  loadSessions();
};
window.setSessionLimit = (val) => {
  sessState.limit = parseInt(val, 10);
  sessState.offset = 0;
  loadSessions();
};
window.onSessionSort = (col) => {
  if (sessState.sortBy === col) {
    sessState.sortOrder = sessState.sortOrder === 'asc' ? 'desc' : 'asc';
  } else {
    sessState.sortBy = col;
    sessState.sortOrder = 'desc';
  }
  sessState.offset = 0;
  loadSessions();
};

$('#sess-search').addEventListener('input', debounce(() => {
  sessState.search = $('#sess-search').value.trim();
  sessState.offset = 0;
  loadSessions();
}, 300));

function debounce(fn, ms) {
  let t;
  return () => { clearTimeout(t); t = setTimeout(fn, ms); };
}

window.showSessionDetail = async (id) => {
  try {
    const s = await api.session(id);
    showDrawer('Session Detail', JSON.stringify(s, null, 2));
  } catch (err) { alert(err.message); }
};
window.deleteSession = async (id) => {
  if (!confirm(`Delete session ${id}?`)) return;
  try { await api.delSession(id); loadSessions(); } catch (err) { alert(err.message); }
};

// ── Memory ─────────────────────────────────────────────────────────────────
const memState = { limit: 50, offset: 0, search: '', userId: null, sortBy: 'updated_at', sortOrder: 'desc', total: 0, items: [] };

async function loadMemory() {
  const el = $('#memory-content');
  const countEl = $('#mem-count');
  try {
    const params = new URLSearchParams();
    params.set('limit', String(memState.limit));
    params.set('offset', String(memState.offset));
    if (memState.search) params.set('search', memState.search);
    if (memState.userId) params.set('user_id', memState.userId);
    params.set('sort_by', memState.sortBy);
    params.set('sort_order', memState.sortOrder);
    const data = await api.memory(params.toString());
    state.memoryLoaded = true;
    memState.total = data.total || 0;
    memState.items = data.items || [];
    el.innerHTML = renderMemoryTable(memState.items);
    countEl.textContent = `${memState.total} total`;
    renderMemoryPagination();
  } catch (err) {
    el.innerHTML = `<div class="error-msg">${esc(err.message)}</div>`;
    countEl.textContent = '';
  }
}

const MEMORY_COLUMNS = [
  { key: 'category', label: 'Category' },
  { key: 'key', label: 'Key' },
  { key: 'value', label: 'Value', sortable: false },
  { key: 'source', label: 'Source' },
  { key: 'confidence', label: 'Confidence' },
  { key: 'updated_at', label: 'Updated' },
];

function renderMemoryTable(items) {
  if (!items.length) return '<div class="placeholder">No facts.</div>';
  let html = '<table><thead><tr>';
  for (const col of MEMORY_COLUMNS) {
    const isSorted = memState.sortBy === col.key;
    const cls = col.sortable !== false ? `sortable ${isSorted ? 'sort-' + memState.sortOrder : ''}` : '';
    html += `<th class="${cls}" onclick="${col.sortable !== false ? `onMemorySort('${esc(col.key)}')` : ''}">${esc(col.label)}</th>`;
  }
  html += '</tr></thead><tbody>';
  for (const f of items) {
    html += `<tr>
      <td>${esc(f.category)}</td>
      <td>${esc(f.key)}</td>
      <td class="value-cell">${esc(f.value)}</td>
      <td>${esc(f.source)}</td>
      <td>${f.confidence}</td>
      <td>${fmtDate(f.updated_at)}</td>
    </tr>`;
  }
  html += '</tbody></table>';
  return html;
}

function renderMemoryPagination() {
  const el = $('#mem-pagination');
  const start = memState.total ? memState.offset + 1 : 0;
  const end = Math.min(memState.offset + memState.limit, memState.total);
  const hasPrev = memState.offset > 0;
  const hasNext = memState.offset + memState.limit < memState.total;
  el.innerHTML = `
    <button class="pagination-btn" onclick="changeMemoryPage(-1)" ${hasPrev ? '' : 'disabled'}>Prev</button>
    <span class="pagination-info">${start}–${end} of ${memState.total}</span>
    <button class="pagination-btn" onclick="changeMemoryPage(1)" ${hasNext ? '' : 'disabled'}>Next</button>
    <select class="pagination-btn" onchange="setMemoryLimit(this.value)" style="padding:4px 6px; min-width:60px;">
      <option value="10" ${memState.limit===10?'selected':''}>10</option>
      <option value="25" ${memState.limit===25?'selected':''}>25</option>
      <option value="50" ${memState.limit===50?'selected':''}>50</option>
      <option value="100" ${memState.limit===100?'selected':''}>100</option>
    </select>
  `;
}

window.changeMemoryPage = (delta) => {
  memState.offset = Math.max(0, memState.offset + delta * memState.limit);
  loadMemory();
};
window.setMemoryLimit = (val) => {
  memState.limit = parseInt(val, 10);
  memState.offset = 0;
  loadMemory();
};
window.onMemorySort = (col) => {
  if (memState.sortBy === col) {
    memState.sortOrder = memState.sortOrder === 'asc' ? 'desc' : 'asc';
  } else {
    memState.sortBy = col;
    memState.sortOrder = 'desc';
  }
  memState.offset = 0;
  loadMemory();
};

$('#mem-search').addEventListener('input', debounce(() => {
  memState.search = $('#mem-search').value.trim();
  memState.offset = 0;
  loadMemory();
}, 300));

// ── Profiles ──────────────────────────────────────────────────────────────
async function loadProfiles() {
  const el = $('#profiles-content');
  try {
    const profiles = await api.profiles();
    state.profilesLoaded = true;
    el.innerHTML = renderProfiles(profiles);
  } catch (err) {
    el.innerHTML = `<div class="error-msg">${esc(err.message)}</div>`;
  }
}

function renderProfiles(profiles) {
  if (!profiles.length) return '<div class="placeholder">No profiles.</div>';
  let html = '<table><thead><tr>';
  ['ID','Name','Description','Admin Only','Actions'].forEach(h => html += `<th>${h}</th>`);
  html += '</tr></thead><tbody>';
  for (const p of profiles) {
    html += `<tr>
      <td class="mono">${esc(p.id)}</td>
      <td>${esc(p.name)}</td>
      <td>${esc(p.description)}</td>
      <td>
        <label class="switch">
          <input type="checkbox" ${p.is_admin_only ? 'checked' : ''} onchange="toggleProfile('${esc(p.id)}', this.checked)">
          <span class="slider"></span>
        </label>
      </td>
      <td><button onclick="editProfile('${esc(p.id)}')">Edit</button></td>
    </tr>`;
  }
  html += '</tbody></table>';
  return html;
}

window.toggleProfile = async (id, value) => {
  try { await api.updateProfile(id, value); loadProfiles(); } catch (err) { alert(err.message); }
};

// ── Profile Edit ───────────────────────────────────────────────────────────
window.editProfile = async (id) => {
  try {
    const p = await api.profile(id);
    showProfileDrawer(id, p);
  } catch (err) {
    alert(err.message);
  }
};

function showProfileDrawer(id, p) {
  const existing = document.querySelector('.drawer-overlay');
  if (existing) existing.remove();

  const modelVal = Array.isArray(p.model) ? p.model.join('\n') : String(p.model || '');
  const toolsVal = Array.isArray(p.enabled_tools) ? p.enabled_tools.join('\n') : String(p.enabled_tools || '');
  const subagentToolsVal = Array.isArray(p.subagent_tools) ? p.subagent_tools.join('\n') : String(p.subagent_tools || '');
  const ctxVal = Array.isArray(p.context_providers) ? p.context_providers.join('\n') : String(p.context_providers || '');

  const overlay = document.createElement('div');
  overlay.className = 'drawer-overlay';
  overlay.innerHTML = `
    <div class="drawer" style="width:600px;max-width:95vw;">
      <h3>Edit Profile: ${esc(p.id)}</h3>
      <div id="profile-form" style="display:flex;flex-direction:column;gap:12px;">

        <section><h4 class="group-title">Basic</h4>
          <label>Name</label><input id="pf-name" value="${esc(p.name)}">
          <label>Description</label><input id="pf-description" value="${esc(p.description)}">
          <label>Short Description</label><input id="pf-short_desc" value="${esc(p.short_description||'')}" placeholder="One-line summary...">
          <label>LLM Backend</label><input id="pf-backend" value="${esc(p.llm_backend)}">
        </section>

        <section><h4 class="group-title">Model & Generation</h4>
          <label>Model (one per line, priority order)</label>
          <textarea id="pf-model" rows="4" style="font-family:monospace;">${esc(modelVal)}</textarea>
          <label>Temperature</label><input id="pf-temp" type="number" step="0.01" value="${p.temperature}" style="width:100px;">
          <label>Top-K</label><input id="pf-topk" type="number" value="${p.top_k??''}" style="width:100px;">
          <label>Top-P</label><input id="pf-topp" type="number" step="0.01" value="${p.top_p??''}" style="width:100px;">
          <label>Max Iterations</label><input id="pf-max_iter" type="number" value="${p.max_iterations}" style="width:100px;">
          <label>Num Threads</label><input id="pf-num_thread" type="number" value="${p.num_thread??''}" style="width:100px;">
        </section>

        <section><h4 class="group-title">Thinking</h4>
          <label><input type="checkbox" id="pf-think" ${p.think_enabled?'checked':''}> Think Enabled</label>
          <label><input type="checkbox" id="pf-budget" ${p.iteration_budget_enabled?'checked':''}> Iteration Budget</label>
          <label><input type="checkbox" id="pf-goal" ${p.goal_anchoring_enabled?'checked':''}> Goal Anchoring (interval: <input id="pf-goal_int" type="number" value="${p.goal_anchoring_interval}" style="width:60px;">)</label>
          <label><input type="checkbox" id="pf-stall" ${p.anti_stall_enabled?'checked':''}> Anti-Stall (threshold: <input id="pf-stall_thr" type="number" value="${p.anti_stall_threshold}" style="width:60px;">)</label>
          <label><input type="checkbox" id="pf-validate" ${p.step_validation_enabled?'checked':''}> Step Validation</label>
          <label><input type="checkbox" id="pf-replan" ${p.adaptive_replan_enabled?'checked':''}> Adaptive Replan</label>
        </section>

        <section><h4 class="group-title">Planning</h4>
          <label><input type="checkbox" id="pf-plan" ${p.planning_enabled?'checked':''}> Planning Enabled</label>
          <label><input type="checkbox" id="pf-plan_mand" ${p.planning_mandatory?'checked':''}> Planning Mandatory</label>
          <label><input type="checkbox" id="pf-phase1" ${p.planning_phase1_enabled?'checked':''}> Phase 1 (Analysis)</label>
          <label><input type="checkbox" id="pf-phase2" ${p.planning_phase2_enabled?'checked':''}> Phase 2 (Review)</label>
          <label><input type="checkbox" id="pf-phase3" ${p.planning_phase3_enabled?'checked':''}> Phase 3 (Plan)</label>
        </section>

        <section><h4 class="group-title">Sub-agent</h4>
          <label><input type="checkbox" id="pf-sub_plan" ${p.subagent_planning_enabled?'checked':''}> Sub-agent Planning</label>
          <label>Sub-agent Think Enabled</label>
          <select id="pf-sub_think">
            <option value="null" ${p.subagent_think_enabled===null?'selected':''}>Inherit (null)</option>
            <option value="true" ${p.subagent_think_enabled===true?'selected':''}>True</option>
            <option value="false" ${p.subagent_think_enabled===false?'selected':''}>False</option>
          </select>
          <label>Sub-agent Tools (one per line)</label>
          <textarea id="pf-sub_tools" rows="3" style="font-family:monospace;">${esc(subagentToolsVal)}</textarea>
        </section>

        <section><h4 class="group-title">Tools</h4>
          <label>Enabled Tools (one per line)</label>
          <textarea id="pf-tools" rows="6" style="font-family:monospace;">${esc(toolsVal)}</textarea>
          <label>Context Providers (one per line)</label>
          <textarea id="pf-ctx" rows="3" style="font-family:monospace;">${esc(ctxVal)}</textarea>
        </section>

        <section><h4 class="group-title">System Prompt</h4>
          <textarea id="pf-system" rows="10" style="font-family:monospace;width:100%;">${esc(p.system_prompt||'')}</textarea>
          <label>Sub-agent System Prompt</label>
          <textarea id="pf-sub_system" rows="4" style="font-family:monospace;width:100%;">${esc(p.subagent_system_prompt||'')}</textarea>
        </section>

        <div style="display:flex;gap:8px;margin-top:8px;">
          <button class="btn btn-primary" onclick="saveProfileEdit('${esc(id)}')">Save</button>
          <button onclick="this.closest('.drawer-overlay').remove()">Cancel</button>
        </div>
      </div>
    </div>`;
  overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
}

window.saveProfileEdit = async (id) => {
  const body = {
    name: document.getElementById('pf-name').value,
    description: document.getElementById('pf-description').value,
    short_description: document.getElementById('pf-short_desc').value,
    llm_backend: document.getElementById('pf-backend').value,
    model: document.getElementById('pf-model').value.split('\n').map(s => s.trim()).filter(Boolean),
    temperature: parseFloat(document.getElementById('pf-temp').value),
    top_k: document.getElementById('pf-topk').value ? parseInt(document.getElementById('pf-topk').value, 10) : null,
    top_p: document.getElementById('pf-topp').value ? parseFloat(document.getElementById('pf-topp').value) : null,
    max_iterations: parseInt(document.getElementById('pf-max_iter').value, 10),
    num_thread: document.getElementById('pf-num_thread').value ? parseInt(document.getElementById('pf-num_thread').value, 10) : null,
    think_enabled: document.getElementById('pf-think').checked,
    iteration_budget_enabled: document.getElementById('pf-budget').checked,
    goal_anchoring_enabled: document.getElementById('pf-goal').checked,
    goal_anchoring_interval: parseInt(document.getElementById('pf-goal_int').value, 10),
    anti_stall_enabled: document.getElementById('pf-stall').checked,
    anti_stall_threshold: parseInt(document.getElementById('pf-stall_thr').value, 10),
    step_validation_enabled: document.getElementById('pf-validate').checked,
    adaptive_replan_enabled: document.getElementById('pf-replan').checked,
    planning_enabled: document.getElementById('pf-plan').checked,
    planning_mandatory: document.getElementById('pf-plan_mand').checked,
    planning_phase1_enabled: document.getElementById('pf-phase1').checked,
    planning_phase2_enabled: document.getElementById('pf-phase2').checked,
    planning_phase3_enabled: document.getElementById('pf-phase3').checked,
    subagent_planning_enabled: document.getElementById('pf-sub_plan').checked,
    subagent_think_enabled: (() => {
      const v = document.getElementById('pf-sub_think').value;
      return v === 'null' ? null : v === 'true';
    })(),
    subagent_tools: document.getElementById('pf-sub_tools').value.split('\n').map(s => s.trim()).filter(Boolean),
    enabled_tools: document.getElementById('pf-tools').value.split('\n').map(s => s.trim()).filter(Boolean),
    context_providers: document.getElementById('pf-ctx').value.split('\n').map(s => s.trim()).filter(Boolean),
    system_prompt: document.getElementById('pf-system').value,
    subagent_system_prompt: document.getElementById('pf-sub_system').value,
  };

  try {
    await api.saveProfile(id, body);
    document.querySelector('.drawer-overlay')?.remove();
    loadProfiles();
  } catch (err) {
    alert(err.message);
  }
};

// ── Drawer ─────────────────────────────────────────────────────────────────
function showDrawer(title, content) {
  const existing = document.querySelector('.drawer-overlay');
  if (existing) existing.remove();

  const overlay = document.createElement('div');
  overlay.className = 'drawer-overlay';
  overlay.innerHTML = `
    <div class="drawer">
      <h3>${esc(title)}</h3>
      <pre>${esc(content)}</pre>
      <button onclick="this.closest('.drawer-overlay').remove()">Close</button>
    </div>`;
  overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
}

// ── boot ───────────────────────────────────────────────────────────────────
const hash = location.hash.slice(1);
const initialTab = ['users','sessions','memory','profiles'].includes(hash) ? hash : 'users';
if (initialTab !== 'users') switchTab(initialTab);
else loadUsers();
</script>

</body>
</html>