oliver-metadata-tool/static/js/admin.js
SamoilenkoVadym 3deaa5ef40 Initial commit: Oliver Metadata Tool (FastAPI)
Complete Flask → FastAPI migration with:
- FastAPI app with session auth, Azure AD SSO, rate limiting
- SQLite-backed session store (survives restarts)
- Bulk AI metadata generation with SSE progress
- Admin panel (user management, audit log, AI usage)
- Subpath deployment support (ROOT_PATH config)
- Docker + deploy.sh for production deployment
- Test suite (auth, upload, templates, imports, admin, sessions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 21:23:42 +00:00

265 lines
9 KiB
JavaScript

// Admin Dashboard JavaScript
document.addEventListener('DOMContentLoaded', () => {
loadUsers();
});
function switchTab(tab) {
document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.admin-panel').forEach(p => p.style.display = 'none');
event.target.classList.add('active');
if (tab === 'users') {
document.getElementById('usersPanel').style.display = 'block';
loadUsers();
} else if (tab === 'audit') {
document.getElementById('auditPanel').style.display = 'block';
loadAuditLog();
} else if (tab === 'ai-usage') {
document.getElementById('aiUsagePanel').style.display = 'block';
loadAiUsage();
}
}
// --- Users ---
async function loadUsers() {
try {
const resp = await fetch(BASE_PATH + '/admin/users?include_inactive=true');
const data = await resp.json();
if (data.success) {
renderUsersTable(data.users);
populateAuditUserFilter(data.users);
}
} catch (err) {
console.error('Failed to load users:', err);
}
}
function renderUsersTable(users) {
const tbody = document.getElementById('usersTableBody');
if (!users.length) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#6b7280;">No users found</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td><strong>${escapeHtml(u.username)}</strong></td>
<td>${escapeHtml(u.email || '-')}</td>
<td><span class="badge badge-${u.role}">${u.role}</span></td>
<td>${u.auth_method || 'local'}</td>
<td>${u.last_login ? formatDate(u.last_login) : 'Never'}</td>
<td><span class="badge badge-${u.is_active ? 'active' : 'inactive'}">${u.is_active ? 'Active' : 'Inactive'}</span></td>
<td>
${u.is_active
? `<button class="btn-action danger" onclick="toggleUser(${u.id}, false)">Deactivate</button>`
: `<button class="btn-action" onclick="toggleUser(${u.id}, true)">Activate</button>`
}
<button class="btn-action" onclick="toggleRole(${u.id}, '${u.role}')">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>
</td>
</tr>
`).join('');
}
async function toggleUser(userId, activate) {
try {
const resp = await fetch(`${BASE_PATH}/admin/users/${userId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({is_active: activate ? 1 : 0}),
});
const data = await resp.json();
if (data.success) loadUsers();
else alert(data.error || 'Failed to update user');
} catch (err) {
alert('Error: ' + err.message);
}
}
async function toggleRole(userId, currentRole) {
const newRole = currentRole === 'admin' ? 'user' : 'admin';
if (!confirm(`Change user role to "${newRole}"?`)) return;
try {
const resp = await fetch(`${BASE_PATH}/admin/users/${userId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({role: newRole}),
});
const data = await resp.json();
if (data.success) loadUsers();
else alert(data.error || 'Failed to update role');
} catch (err) {
alert('Error: ' + err.message);
}
}
function showCreateUserModal() {
document.getElementById('createUserModal').style.display = 'flex';
}
function closeCreateUserModal() {
document.getElementById('createUserModal').style.display = 'none';
document.getElementById('newUsername').value = '';
document.getElementById('newEmail').value = '';
document.getElementById('newFullName').value = '';
document.getElementById('newPassword').value = '';
document.getElementById('newRole').value = 'user';
document.getElementById('newAuthMethod').value = 'local';
}
async function createUser() {
const username = document.getElementById('newUsername').value.trim();
if (!username) { alert('Username is required'); return; }
const payload = {
username,
email: document.getElementById('newEmail').value.trim(),
full_name: document.getElementById('newFullName').value.trim(),
password: document.getElementById('newPassword').value || null,
role: document.getElementById('newRole').value,
auth_method: document.getElementById('newAuthMethod').value,
};
try {
const resp = await fetch(BASE_PATH + '/admin/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
const data = await resp.json();
if (data.success) {
closeCreateUserModal();
loadUsers();
} else {
alert(data.error || 'Failed to create user');
}
} catch (err) {
alert('Error: ' + err.message);
}
}
// --- Audit Log ---
function populateAuditUserFilter(users) {
const select = document.getElementById('auditUserFilter');
const currentVal = select.value;
select.innerHTML = '<option value="">All Users</option>';
users.forEach(u => {
select.innerHTML += `<option value="${u.id}">${escapeHtml(u.username)}</option>`;
});
select.value = currentVal;
}
async function loadAuditLog() {
const userId = document.getElementById('auditUserFilter').value;
let url = BASE_PATH + '/admin/audit?limit=200';
if (userId) url += `&user_id=${userId}`;
try {
const resp = await fetch(url);
const data = await resp.json();
if (data.success) {
renderAuditTable(data.entries);
}
} catch (err) {
console.error('Failed to load audit log:', err);
}
}
function renderAuditTable(entries) {
const tbody = document.getElementById('auditTableBody');
if (!entries.length) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#6b7280;">No audit entries</td></tr>';
return;
}
tbody.innerHTML = entries.map(e => `
<tr>
<td style="white-space:nowrap;">${formatDate(e.timestamp)}</td>
<td>${escapeHtml(e.username || 'Unknown')}</td>
<td><strong>${escapeHtml(e.action)}</strong></td>
<td style="max-width:400px;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(e.details || '-')}</td>
</tr>
`).join('');
}
// --- AI Usage ---
async function loadAiUsage() {
try {
const resp = await fetch(BASE_PATH + '/admin/ai-usage');
const data = await resp.json();
if (data.success) {
renderAiStats(data.stats);
renderAiUsageTable(data.by_user);
}
} catch (err) {
console.error('Failed to load AI usage:', err);
}
}
function renderAiStats(stats) {
const grid = document.getElementById('aiStatsGrid');
grid.innerHTML = `
<div class="ai-stat-card">
<div class="ai-stat-value">${stats.total_requests || 0}</div>
<div class="ai-stat-label">Total Requests</div>
</div>
<div class="ai-stat-card">
<div class="ai-stat-value">${(stats.total_tokens || 0).toLocaleString()}</div>
<div class="ai-stat-label">Total Tokens</div>
</div>
<div class="ai-stat-card">
<div class="ai-stat-value">${stats.requests_24h || 0}</div>
<div class="ai-stat-label">Requests (24h)</div>
</div>
<div class="ai-stat-card">
<div class="ai-stat-value">${(stats.tokens_24h || 0).toLocaleString()}</div>
<div class="ai-stat-label">Tokens (24h)</div>
</div>
<div class="ai-stat-card">
<div class="ai-stat-value">${stats.requests_7d || 0}</div>
<div class="ai-stat-label">Requests (7d)</div>
</div>
<div class="ai-stat-card">
<div class="ai-stat-value">${(stats.tokens_7d || 0).toLocaleString()}</div>
<div class="ai-stat-label">Tokens (7d)</div>
</div>
`;
}
function renderAiUsageTable(byUser) {
const tbody = document.getElementById('aiUsageTableBody');
if (!byUser.length) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:#6b7280;">No AI usage data</td></tr>';
return;
}
tbody.innerHTML = byUser.map(u => `
<tr>
<td><strong>${escapeHtml(u.username)}</strong></td>
<td>${u.request_count}</td>
<td>${(u.total_tokens || 0).toLocaleString()}</td>
<td>${u.last_used ? formatDate(u.last_used) : '-'}</td>
</tr>
`).join('');
}
// --- Helpers ---
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const d = new Date(dateStr);
return d.toLocaleString();
} catch {
return dateStr;
}
}