/**
* 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 →</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()">×</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];
}