OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / admin-ui.js

1058 lines · javascript ref:
/**
 * Admin UI
 *
 * Admin dashboard for managing users, repositories, and system settings
 */

import { renderPage } from './page-layout.js'

const PAGE_CSS = `
    /* Tabs */
    .tabs {
      display: flex;
      gap: 5px;
      border-bottom: 1px solid #30363d;
      margin-bottom: 30px;
    }
    .tab {
      padding: 12px 24px;
      background: transparent;
      border: none;
      color: #8b949e;
      cursor: pointer;
      border-radius: 6px 6px 0 0;
      font-size: 16px;
      font-weight: 500;
    }
    .tab.active {
      background: #161b22;
      border: 1px solid #30363d;
      border-bottom: none;
      color: #c9d1d9;
    }
    .tab:hover:not(.active) { color: #c9d1d9; }

    /* Tab Content */
    .tab-content { display: none; }
    .tab-content.active { display: block; }

    /* Stats Grid */
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 20px;
      margin-bottom: 40px;
    }
    .stat-card {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 24px;
    }
    .stat-value {
      font-size: 36px;
      font-weight: bold;
      color: #58a6ff;
      margin-bottom: 8px;
    }
    .stat-label {
      color: #8b949e;
      font-size: 14px;
    }
    .stat-card.highlight {
      border-color: #238636;
    }

    /* Tables */
    .data-table {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      overflow: hidden;
    }
    .table-header {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 15px;
      padding: 15px 20px;
      background: #0d1117;
      border-bottom: 1px solid #30363d;
      font-weight: 600;
      font-size: 12px;
      text-transform: uppercase;
      color: #8b949e;
    }
    .table-row {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 15px;
      padding: 15px 20px;
      border-bottom: 1px solid #30363d;
      align-items: center;
    }
    .table-row:last-child { border-bottom: none; }
    .table-row:hover { background: #0d1117; }

    /* Badges */
    .badge {
      display: inline-block;
      padding: 4px 8px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: 600;
    }
    .badge.admin { background: #238636; color: white; }
    .badge.write { background: #58a6ff; color: white; }
    .badge.read { background: #6e7681; color: white; }
    .badge.public { background: #1f6feb; color: white; }
    .badge.private { background: #8b949e; color: white; }

    /* Forms — page-specific */
    .form-section {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 20px;
      margin-bottom: 20px;
    }
    .form-group {
      margin-bottom: 15px;
    }
    .form-group label {
      display: block;
      margin-bottom: 6px;
      font-weight: 500;
      font-size: 14px;
    }
    .form-group input,
    .form-group select {
      width: 100%;
      background: #0d1117;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 8px 12px;
      color: #c9d1d9;
      font-size: 14px;
    }
    .form-group input:focus,
    .form-group select:focus {
      outline: none;
      border-color: #58a6ff;
    }
    .checkbox-group {
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      margin-top: 8px;
    }
    .checkbox-label {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 14px;
    }

    /* Empty State */
    .empty-state {
      text-align: center;
      padding: 60px 20px;
      color: #8b949e;
    }
    .empty-state-icon {
      font-size: 48px;
      margin-bottom: 10px;
    }

    /* Activity List */
    .activity-list {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
    }
    .activity-item {
      padding: 15px 20px;
      border-bottom: 1px solid #30363d;
      display: flex;
      gap: 15px;
      align-items: flex-start;
    }
    .activity-item:last-child { border-bottom: none; }
    .activity-icon {
      font-size: 20px;
      padding: 8px;
      background: #0d1117;
      border-radius: 6px;
    }
    .activity-content { flex: 1; }
    .activity-title { font-weight: 500; margin-bottom: 4px; }
    .activity-time {
      font-size: 12px;
      color: #8b949e;
    }

    /* Modal */
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.7);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1000;
    }
    .modal-content {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 12px;
      width: 90%;
      max-width: 600px;
      max-height: 90vh;
      overflow-y: auto;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20px;
      border-bottom: 1px solid #30363d;
    }
    .modal-header h2 { margin: 0; font-size: 20px; }
    .modal-close {
      background: none;
      border: none;
      color: #8b949e;
      font-size: 24px;
      cursor: pointer;
      padding: 0;
      width: 32px;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .modal-close:hover { color: #c9d1d9; background: none; }
    .modal-body {
      padding: 20px;
    }
    .modal-body .form-section {
      margin-bottom: 20px;
      background: #0d1117;
      padding: 15px;
      border-radius: 6px;
    }
    .modal-body .form-section h3 {
      margin: 0 0 10px 0;
      font-size: 14px;
      color: #58a6ff;
    }
    .modal-footer {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      padding: 15px 20px;
      border-top: 1px solid #30363d;
    }
    .form-select, .form-input {
      width: 100%;
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 8px 12px;
      color: #c9d1d9;
      font-size: 14px;
    }
    .form-select:focus, .form-input:focus {
      outline: none;
      border-color: #58a6ff;
    }
    .help-text {
      font-size: 12px;
      color: #8b949e;
      margin-top: 5px;
    }
    .collaborator-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 12px;
      background: #161b22;
      border-radius: 4px;
      margin-bottom: 5px;
    }
    .collaborator-item .user-info {
      font-family: monospace;
      font-size: 12px;
    }
    .collaborator-item .badge { margin-left: 10px; }
    .badge.internal { background: #f0883e; color: white; }
    .badge.archived { background: #8b949e; color: white; }
`

export function renderAdminDashboard(stats = {}, pathPrefix = '') {
  const {
    totalUsers = 0,
    totalRepos = 0,
    totalIssues = 0,
    recentActivity = [],
    identitySecured = false,
    federationKey = null,
    federationDiscoveryKey = null,
    peerCount = 0
  } = stats

  const content = `

    ${!identitySecured ? `
    <div style="background:#3d1f00;border:1px solid #6e3500;padding:12px 16px;border-radius:6px;margin-bottom:20px;">
      <strong style="color:#f85149;">Identity not secured.</strong>
      <span style="color:#c9d1d9;">Run <code style="background:#0d1117;padding:2px 6px;border-radius:3px;">pgh identity setup</code> to create an encrypted Keet identity.</span>
    </div>` : ''}

    <!-- Tabs -->
    <div class="tabs">
      <button class="tab active" onclick="switchTab('dashboard')">Dashboard</button>
      <button class="tab" onclick="switchTab('users')">Users</button>
      <button class="tab" onclick="switchTab('repos')">Repositories</button>
      <button class="tab" onclick="switchTab('identities')">Identities</button>
    </div>

    <!-- Dashboard Tab -->
    <div id="tab-dashboard" class="tab-content active">
      <div class="stats-grid">
        <div class="stat-card highlight">
          <div class="stat-value">${totalUsers}</div>
          <div class="stat-label">Total Users</div>
        </div>
        <div class="stat-card">
          <div class="stat-value">${totalRepos}</div>
          <div class="stat-label">Repositories</div>
        </div>
        <div class="stat-card">
          <div class="stat-value">${totalIssues}</div>
          <div class="stat-label">Total Issues</div>
        </div>
      </div>

      ${federationKey ? `
      <div style="margin-bottom: 30px; padding: 16px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
        <h3 style="margin-bottom: 12px; color: #58a6ff;">Federation</h3>
        <div style="margin-bottom: 8px;">
          <span style="color: #8b949e;">Identity Registry Bootstrap Key:</span>
          <code style="display: block; margin-top: 4px; padding: 8px; background: #0d1117; border-radius: 4px; font-size: 12px; word-break: break-all; color: #c9d1d9;">${federationKey}</code>
        </div>
        <div>
          <span style="color: #8b949e;">Discovery Key:</span>
          <code style="display: block; margin-top: 4px; padding: 8px; background: #0d1117; border-radius: 4px; font-size: 12px; word-break: break-all; color: #c9d1d9;">${federationDiscoveryKey || 'N/A'}</code>
        </div>
        <p style="margin-top: 10px; font-size: 13px; color: #8b949e;">Share the bootstrap key with remote servers to federate identity registries.</p>
      </div>
      ` : ''}

      <div style="margin-bottom: 30px; padding: 16px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <h3 style="color: #58a6ff;">Server Peering</h3>
          <a href="${pathPrefix}/peers" style="color: #58a6ff; text-decoration: none; font-size: 14px; padding: 4px 12px; border: 1px solid #30363d; border-radius: 6px; background: #0d1117;">Manage Peers &rarr;</a>
        </div>
        <div style="margin-top: 10px; display: flex; gap: 30px;">
          <div>
            <span style="font-size: 24px; font-weight: bold; color: ${peerCount > 0 ? '#3fb950' : '#8b949e'};">${peerCount}</span>
            <span style="color: #8b949e; margin-left: 6px;">peered server${peerCount !== 1 ? 's' : ''}</span>
          </div>
          <div id="peer-sync-status" style="color: #8b949e; font-size: 13px; display: flex; align-items: center;">Loading sync status...</div>
        </div>
      </div>

      <h2 style="margin-bottom: 20px;">Recent Activity</h2>
      <div class="activity-list" id="activity-list">
        ${recentActivity.length > 0 ? recentActivity.map(renderActivity).join('') : `
          <div class="empty-state">
            <div class="empty-state-icon">📊</div>
            <div>No recent activity</div>
          </div>
        `}
      </div>
    </div>

    <!-- Users Tab -->
    <div id="tab-users" class="tab-content">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>User Management</h2>
        <button onclick="refreshUsers()">🔄 Refresh</button>
      </div>

      <div class="data-table">
        <div class="table-header">
          <div>User ID</div>
          <div>Name</div>
          <div>Identities</div>
          <div>Permissions</div>
          <div>Actions</div>
        </div>
        <div id="users-list">
          <div class="empty-state">
            <div class="empty-state-icon">👥</div>
            <div>Loading users...</div>
          </div>
        </div>
      </div>

      <div class="form-section" style="margin-top: 30px;">
        <h3 style="margin-bottom: 15px;">Grant Permissions</h3>
        <form id="grant-permissions-form" onsubmit="grantPermissions(event)">
          <div class="form-group">
            <label for="user-id">User ID:</label>
            <input type="text" id="user-id" required placeholder="Enter user ID">
          </div>
          <div class="form-group">
            <label>Permissions:</label>
            <div class="checkbox-group">
              <label class="checkbox-label">
                <input type="checkbox" name="permission" value="read" checked> Read
              </label>
              <label class="checkbox-label">
                <input type="checkbox" name="permission" value="create-issue"> Create Issues
              </label>
              <label class="checkbox-label">
                <input type="checkbox" name="permission" value="manage-issues"> Manage Issues
              </label>
              <label class="checkbox-label">
                <input type="checkbox" name="permission" value="write"> Write (Push)
              </label>
              <label class="checkbox-label">
                <input type="checkbox" name="permission" value="admin"> Admin
              </label>
            </div>
          </div>
          <div class="form-group">
            <label for="repo-name">Repository (optional - leave empty for global):</label>
            <input type="text" id="repo-name" placeholder="e.g., hypercore">
          </div>
          <button type="submit">Grant Permissions</button>
        </form>
      </div>
    </div>

    <!-- Repositories Tab -->
    <div id="tab-repos" class="tab-content">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>Repository Management</h2>
        <button onclick="refreshRepos()">🔄 Refresh</button>
      </div>

      <div class="data-table">
        <div class="table-header">
          <div>Name</div>
          <div>Owner</div>
          <div>Visibility</div>
          <div>Status</div>
          <div>Actions</div>
        </div>
        <div id="repos-list">
          <div class="empty-state">
            <div class="empty-state-icon">📦</div>
            <div>Loading repositories...</div>
          </div>
        </div>
      </div>
    </div>

    <!-- Repo Edit Modal -->
    <div id="repo-modal" class="modal-overlay" style="display: none;">
      <div class="modal-content">
        <div class="modal-header">
          <h2 id="repo-modal-title">Edit Repository</h2>
          <button class="modal-close" onclick="closeRepoModal()">&times;</button>
        </div>
        <div class="modal-body">
          <input type="hidden" id="edit-repo-name">

          <div class="form-section">
            <h3>Visibility</h3>
            <select id="edit-visibility" class="form-select">
              <option value="public">Public - Anyone can view</option>
              <option value="internal">Internal - Authenticated users only</option>
              <option value="private">Private - Collaborators only</option>
            </select>
          </div>

          <div class="form-section">
            <h3>Owner</h3>
            <div style="display: flex; gap: 10px;">
              <input type="text" id="edit-owner" class="form-input" placeholder="User ID" style="flex: 1;">
              <button class="secondary" onclick="clearOwner()">Clear</button>
            </div>
            <div class="help-text">Enter the User ID of the repo owner (visible in their profile)</div>
          </div>

          <div class="form-section">
            <h3>Collaborators</h3>
            <div id="collaborators-list" style="margin-bottom: 10px;"></div>
            <div style="display: flex; gap: 10px;">
              <input type="text" id="new-collaborator-id" class="form-input" placeholder="User ID" style="flex: 1;">
              <select id="new-collaborator-perms" class="form-select" style="width: 150px;">
                <option value="read">Read</option>
                <option value="write">Write</option>
                <option value="admin">Admin</option>
              </select>
              <button onclick="addCollaborator()">Add</button>
            </div>
          </div>

          <div class="form-section">
            <h3>Archive Status</h3>
            <label class="checkbox-label">
              <input type="checkbox" id="edit-archived">
              <span>Repository is archived (read-only, no new issues/commits)</span>
            </label>
          </div>
        </div>
        <div class="modal-footer">
          <button class="secondary" onclick="closeRepoModal()">Cancel</button>
          <button onclick="saveRepoSettings()">Save Changes</button>
        </div>
      </div>
    </div>

    <!-- Identities Tab -->
    <div id="tab-identities" class="tab-content">
      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
        <h2>Identity Management</h2>
        <button onclick="refreshIdentities()">🔄 Refresh</button>
      </div>

      <div class="data-table">
        <div class="table-header">
          <div>Method</div>
          <div>Identifier</div>
          <div>User</div>
          <div>Created</div>
          <div>Actions</div>
        </div>
        <div id="identities-list">
          <div class="empty-state">
            <div class="empty-state-icon">🔑</div>
            <div>Loading identities...</div>
          </div>
        </div>
      </div>
    </div>
  </div>

  `

  const scripts = `
    let currentTab = 'dashboard';

    // Get base path (server path prefix, not page path)
    function getBasePath() {
      return '${pathPrefix}';
    }

    // Switch tabs
    function switchTab(tab) {
      currentTab = tab;
      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
      document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));

      event.target.classList.add('active');
      document.getElementById('tab-' + tab).classList.add('active');

      // Load data when switching tabs
      if (tab === 'users') refreshUsers();
      if (tab === 'repos') refreshRepos();
      if (tab === 'identities') refreshIdentities();
    }

    // Format bytes
    function formatBytes(bytes) {
      if (bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
    }

    // Render activity item
    function renderActivity(activity) {
      return \`
        <div class="activity-item">
          <div class="activity-icon">\${activity.icon || '📝'}</div>
          <div class="activity-content">
            <div class="activity-title">\${activity.title}</div>
            <div class="activity-time">\${new Date(activity.timestamp).toLocaleString()}</div>
          </div>
        </div>
      \`;
    }

    // Load users
    async function refreshUsers() {
      try {
        const response = await window.fetch(getBasePath() + '/api/admin/users');
        const users = await response.json();

        if (users.length === 0) {
          document.getElementById('users-list').innerHTML = \`
            <div class="empty-state">
              <div class="empty-state-icon">👥</div>
              <div>No users found</div>
            </div>
          \`;
          return;
        }

        document.getElementById('users-list').innerHTML = users.map(user => \`
          <div class="table-row" style="\${user.deactivatedAt ? 'opacity: 0.5;' : ''}">
            <div style="font-family: monospace; font-size: 12px;">\${user.userId ? user.userId.substring(0, 12) + '...' : 'N/A'}</div>
            <div>
              \${user.name || 'Unnamed'}
              \${user.deactivatedAt ? '<span class="badge" style="background:#da3633;color:white;margin-left:6px;">Deactivated</span>' : ''}
            </div>
            <div>\${user.identities?.length || 0} identities</div>
            <div>
              \${(user.globalPermissions || []).map(p => \`<span class="badge \${p}">\${p}</span>\`).join(' ')}
            </div>
            <div style="display:flex;gap:4px;">
              <button class="secondary small" onclick="viewUser('\${user.userId}')" \${!user.userId ? 'disabled' : ''}>View</button>
              \${user.deactivatedAt
                ? \`<button class="secondary small" style="color:#3fb950;" onclick="reactivateUser('\${user.userId}')">Reactivate</button>\`
                : \`<button class="secondary small" style="color:#da3633;" onclick="deactivateUser('\${user.userId}', '\${(user.name || 'Unnamed').replace(/'/g, "\\\\'")}')">Deactivate</button>\`
              }
            </div>
          </div>
        \`).join('');
      } catch (err) {
        console.error('Failed to load users:', err);
        document.getElementById('users-list').innerHTML = \`
          <div class="empty-state">
            <div class="empty-state-icon">❌</div>
            <div>Error loading users</div>
          </div>
        \`;
      }
    }

    // Store repos data for modal
    let reposData = [];

    // Load repos
    async function refreshRepos() {
      try {
        const response = await window.fetch(getBasePath() + '/api/repos');
        const repos = await response.json();
        reposData = repos;

        if (repos.length === 0) {
          document.getElementById('repos-list').innerHTML = \`
            <div class="empty-state">
              <div class="empty-state-icon">📦</div>
              <div>No repositories found</div>
            </div>
          \`;
          return;
        }

        document.getElementById('repos-list').innerHTML = repos.map(repo => \`
          <div class="table-row">
            <div>
              <strong>\${repo.displayName || repo.name}</strong>
              <div style="font-size: 11px; color: #8b949e;">\${repo.namespace || 'unknown'}/\${repo.name}</div>
            </div>
            <div style="font-family: monospace; font-size: 11px;">
              \${repo.owner ? repo.owner.substring(0, 12) + '...' : '<span style="color: #8b949e;">Not set</span>'}
            </div>
            <div>
              <span class="badge \${repo.visibility || 'public'}">\${repo.visibility || 'public'}</span>
            </div>
            <div>
              \${repo.archived ? '<span class="badge archived">Archived</span>' : '<span style="color: #238636;">Active</span>'}
              \${repo.linked ? ' <span class="badge" style="background: #1f6feb; color: white;">Linked</span>' : ''}
            </div>
            <div style="display: flex; gap: 5px;">
              <button class="secondary small" onclick="openRepoModal('\${repo.name}')">⚙️ Edit</button>
              <button class="secondary small" onclick="location.href=getBasePath() + '/\${repo.namespace || 'unknown'}/\${repo.name}'">View</button>
            </div>
          </div>
        \`).join('');
      } catch (err) {
        console.error('Failed to load repos:', err);
        document.getElementById('repos-list').innerHTML = \`
          <div class="empty-state">
            <div class="empty-state-icon">❌</div>
            <div>Error loading repositories</div>
          </div>
        \`;
      }
    }

    // Current repo being edited
    let currentEditRepo = null;

    // Open repo edit modal
    async function openRepoModal(repoName) {
      currentEditRepo = reposData.find(r => r.name === repoName);
      if (!currentEditRepo) {
        alert('Repository not found');
        return;
      }

      document.getElementById('repo-modal-title').textContent = 'Edit: ' + (currentEditRepo.displayName || currentEditRepo.name);
      document.getElementById('edit-repo-name').value = repoName;
      document.getElementById('edit-visibility').value = currentEditRepo.visibility || 'public';
      document.getElementById('edit-owner').value = currentEditRepo.owner || '';
      document.getElementById('edit-archived').checked = currentEditRepo.archived || false;

      // Load collaborators
      await loadCollaborators(repoName);

      document.getElementById('repo-modal').style.display = 'flex';
    }

    // Close repo modal
    function closeRepoModal() {
      document.getElementById('repo-modal').style.display = 'none';
      currentEditRepo = null;
    }

    // Load collaborators for repo
    async function loadCollaborators(repoName) {
      const listEl = document.getElementById('collaborators-list');

      if (!currentEditRepo.collaborators || currentEditRepo.collaborators.length === 0) {
        listEl.innerHTML = '<div style="color: #8b949e; font-size: 13px;">No collaborators</div>';
        return;
      }

      listEl.innerHTML = currentEditRepo.collaborators.map(collab => \`
        <div class="collaborator-item">
          <div>
            <span class="user-info">\${collab.userId.substring(0, 16)}...</span>
            <span class="badge \${collab.permissions?.[0] || 'read'}">\${collab.permissions?.join(', ') || 'read'}</span>
          </div>
          <button class="danger small" onclick="removeCollaborator('\${collab.userId}')">Remove</button>
        </div>
      \`).join('');
    }

    // Add collaborator
    async function addCollaborator() {
      const userId = document.getElementById('new-collaborator-id').value.trim();
      const permission = document.getElementById('new-collaborator-perms').value;
      const repoName = document.getElementById('edit-repo-name').value;

      if (!userId) {
        alert('Please enter a User ID');
        return;
      }

      try {
        const response = await window.fetch(getBasePath() + \`/api/admin/repos/\${repoName}/collaborators\`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId, permissions: [permission] })
        });

        if (response.ok) {
          // Update local data and refresh display
          if (!currentEditRepo.collaborators) currentEditRepo.collaborators = [];
          currentEditRepo.collaborators.push({ userId, permissions: [permission] });
          await loadCollaborators(repoName);
          document.getElementById('new-collaborator-id').value = '';
        } else {
          const error = await response.json();
          alert('Error: ' + error.error);
        }
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }

    // Remove collaborator
    async function removeCollaborator(userId) {
      if (!confirm('Remove this collaborator?')) return;

      const repoName = document.getElementById('edit-repo-name').value;

      try {
        const response = await window.fetch(getBasePath() + \`/api/admin/repos/\${repoName}/collaborators/\${userId}\`, {
          method: 'DELETE'
        });

        if (response.ok) {
          // Update local data and refresh display
          currentEditRepo.collaborators = currentEditRepo.collaborators.filter(c => c.userId !== userId);
          await loadCollaborators(repoName);
        } else {
          const error = await response.json();
          alert('Error: ' + error.error);
        }
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }

    // Clear owner
    function clearOwner() {
      document.getElementById('edit-owner').value = '';
    }

    // Save repo settings
    async function saveRepoSettings() {
      const repoName = document.getElementById('edit-repo-name').value;
      const visibility = document.getElementById('edit-visibility').value;
      const owner = document.getElementById('edit-owner').value.trim();
      const archived = document.getElementById('edit-archived').checked;

      let hasErrors = false;

      // Update visibility
      try {
        const visResponse = await window.fetch(getBasePath() + \`/api/admin/repos/\${repoName}/visibility\`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ visibility })
        });
        if (!visResponse.ok) {
          const error = await visResponse.json();
          console.error('Visibility update failed:', error);
          hasErrors = true;
        }
      } catch (err) {
        console.error('Visibility update error:', err);
        hasErrors = true;
      }

      // Update owner
      try {
        const ownerResponse = await window.fetch(getBasePath() + \`/api/admin/repos/\${repoName}/owner\`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId: owner || null })
        });
        if (!ownerResponse.ok) {
          const error = await ownerResponse.json();
          console.error('Owner update failed:', error);
          hasErrors = true;
        }
      } catch (err) {
        console.error('Owner update error:', err);
        hasErrors = true;
      }

      // Update archived status
      try {
        const archiveResponse = await window.fetch(getBasePath() + \`/api/admin/repos/\${repoName}/archived\`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ archived })
        });
        if (!archiveResponse.ok) {
          const error = await archiveResponse.json();
          console.error('Archive update failed:', error);
          hasErrors = true;
        }
      } catch (err) {
        console.error('Archive update error:', err);
        hasErrors = true;
      }

      if (hasErrors) {
        alert('Some settings may not have been saved. Check console for details.');
      } else {
        alert('Repository settings saved successfully!');
      }

      closeRepoModal();
      refreshRepos();
    }

    // Load identities
    async function refreshIdentities() {
      try {
        const response = await window.fetch(getBasePath() + '/api/admin/identities');
        const identities = await response.json();

        if (identities.length === 0) {
          document.getElementById('identities-list').innerHTML = \`
            <div class="empty-state">
              <div class="empty-state-icon">🔑</div>
              <div>No identities found</div>
            </div>
          \`;
          return;
        }

        document.getElementById('identities-list').innerHTML = identities.map(identity => \`
          <div class="table-row">
            <div>\${identity.method}</div>
            <div style="font-family: monospace; font-size: 12px;">\${identity.identifier?.substring(0, 24)}...</div>
            <div>\${identity.userName || 'Unknown'}</div>
            <div style="font-size: 12px;">\${new Date(identity.createdAt).toLocaleDateString()}</div>
            <div>
              <button class="danger small" onclick="revokeIdentity('\${identity.identityId}')">Revoke</button>
            </div>
          </div>
        \`).join('');
      } catch (err) {
        console.error('Failed to load identities:', err);
        document.getElementById('identities-list').innerHTML = \`
          <div class="empty-state">
            <div class="empty-state-icon">❌</div>
            <div>Error loading identities</div>
          </div>
        \`;
      }
    }

    // Grant permissions
    async function grantPermissions(event) {
      event.preventDefault();

      const userId = document.getElementById('user-id').value;
      const repoName = document.getElementById('repo-name').value || null;
      const permissions = Array.from(document.querySelectorAll('input[name="permission"]:checked'))
        .map(cb => cb.value);

      if (permissions.length === 0) {
        alert('Please select at least one permission');
        return;
      }

      try {
        const response = await window.fetch(getBasePath() + \`/api/admin/users/\${userId}/permissions\`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            permissions,
            repoName
          })
        });

        if (response.ok) {
          alert('Permissions granted successfully');
          document.getElementById('grant-permissions-form').reset();
          document.querySelector('input[value="read"]').checked = true;
          refreshUsers();
        } else {
          const error = await response.json();
          alert('Error: ' + error.error);
        }
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }

    // View user details
    async function deactivateUser(userId, name) {
      const reason = prompt('Deactivate user "' + name + '"? Enter reason (or cancel):');
      if (reason === null) return;

      try {
        const resp = await window.fetch(getBasePath() + '/api/admin/users/' + userId + '/deactivate', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ reason: reason || 'Deactivated by admin' })
        });
        const data = await resp.json();
        if (!resp.ok) {
          alert('Error: ' + (data.error || resp.statusText));
          return;
        }
        refreshUsers();
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }

    async function reactivateUser(userId) {
      if (!confirm('Reactivate this user?')) return;

      try {
        const resp = await window.fetch(getBasePath() + '/api/admin/users/' + userId + '/reactivate', {
          method: 'POST'
        });
        const data = await resp.json();
        if (!resp.ok) {
          alert('Error: ' + (data.error || resp.statusText));
          return;
        }
        refreshUsers();
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }

    function viewUser(userId) {
      alert('User details view coming soon for: ' + userId);
    }

    // Load peer sync status on dashboard
    (async function loadPeerStatus() {
      try {
        const response = await window.fetch(getBasePath() + '/api/peer/status');
        const data = await response.json();
        const el = document.getElementById('peer-sync-status');
        if (!el) return;
        if (data.syncActive) {
          el.innerHTML = '<span style="color:#3fb950;">Sync active</span>';
        } else if (data.peerCount > 0) {
          el.innerHTML = '<span style="color:#d29922;">Sync inactive</span>';
        } else {
          el.innerHTML = '<span style="color:#8b949e;">No peers configured</span>';
        }
      } catch (err) {
        const el = document.getElementById('peer-sync-status');
        if (el) el.innerHTML = '<span style="color:#8b949e;">Status unavailable</span>';
      }
    })();

    // Revoke identity
    async function revokeIdentity(identityId) {
      if (!confirm('Are you sure you want to revoke this identity?')) {
        return;
      }

      try {
        const response = await window.fetch(getBasePath() + \`/api/admin/identities/\${identityId}/revoke\`, {
          method: 'POST'
        });

        if (response.ok) {
          alert('Identity revoked successfully');
          refreshIdentities();
        } else {
          const error = await response.json();
          alert('Error: ' + error.error);
        }
      } catch (err) {
        alert('Error: ' + err.message);
      }
    }
  `

  return renderPage('Admin Dashboard', content, { pathPrefix, activePage: 'admin', css: PAGE_CSS, scripts, maxWidth: 1400 })
}

function renderActivity(activity) {
  return `
    <div class="activity-item">
      <div class="activity-icon">${activity.icon || '📝'}</div>
      <div class="activity-content">
        <div class="activity-title">${activity.title}</div>
        <div class="activity-time">${new Date(activity.timestamp).toLocaleString()}</div>
      </div>
    </div>
  `;
}

function formatBytes(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}