|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
|
|
|
<title>Admin Dashboard - COILD Dhravani</title>
|
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='validate.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='admin.css') }}">
|
|
<meta name="csrf-token" content="{{ session['csrf_token'] }}">
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid px-3">
|
|
<div class="d-flex justify-content-between align-items-center pt-3 pb-3">
|
|
<h1 class="mb-0 h4">COILD Dhravani Admin</h1>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<a href="{{ url_for('super_admin.super_admin_interface') }}" class="btn btn-outline-danger">
|
|
<i class="fas fa-user-shield"></i>
|
|
Super Admin
|
|
</a>
|
|
<a href="{{ url_for('index') }}" class="btn btn-outline-primary">Back to Recording</a>
|
|
<a href="{{ url_for('logout') }}" class="btn btn-outline-secondary">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{% if pb_user %}
|
|
<div class="alert alert-info mb-3">
|
|
<p class="mb-0">
|
|
<i class="fas fa-user-shield"></i>
|
|
Admin: {{ pb_user.name or pb_user.email }} ({{ pb_user.email }})
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="admin-container">
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ stats.total_users }}</div>
|
|
<div class="stat-label">Total Users</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ stats.total_languages }}</div>
|
|
<div class="stat-label">Total Languages</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ stats.total_recordings }}</div>
|
|
<div class="stat-label">Total Recordings</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ "%.1f"|format(stats.total_duration / 3600) }}</div>
|
|
<div class="stat-label">Recording Hours</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ verification_rate }}%</div>
|
|
<div class="stat-label">Verification Rate</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ stats.total_transcripts }}</div>
|
|
<div class="stat-label">Total Transcripts</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="admin-card mb-4">
|
|
<h2 class="section-title mb-4">
|
|
<i class="fas fa-language"></i>
|
|
Language Statistics
|
|
</h2>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Language</th>
|
|
<th>Total Users</th>
|
|
<th>Available Transcripts</th>
|
|
<th>Total Recordings</th>
|
|
<th>Recording Hours</th>
|
|
<th>Verification Rate</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for code, lang_stats in stats.languages.items() %}
|
|
<tr>
|
|
<td>{{ languages|selectattr("code", "equalto", code)|map(attribute="name")|first }}</td>
|
|
<td>{{ lang_stats.total_users }}</td>
|
|
<td>{{ lang_stats.available_transcripts }}</td>
|
|
<td>{{ lang_stats.recordings }}</td>
|
|
<td>{{ "%.1f"|format(lang_stats.total_duration / 3600) }}</td>
|
|
<td>{{ "%.1f"|format(100 * lang_stats.verified / lang_stats.recordings if lang_stats.recordings > 0 else 0) }}%</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="admin-card mb-4">
|
|
<h2 class="section-title mb-4">
|
|
<i class="fas fa-users-cog"></i>
|
|
User Management
|
|
</h2>
|
|
|
|
|
|
<div class="search-section mb-4">
|
|
<h3 class="sub-section-title mb-3">
|
|
<i class="fas fa-user-plus"></i>
|
|
Add/Remove Moderator
|
|
</h3>
|
|
<div class="input-group">
|
|
<input type="email"
|
|
id="searchEmail"
|
|
class="form-control"
|
|
placeholder="Enter email addresses (comma-separated)..."
|
|
multiple
|
|
title="Enter one or more email addresses, separated by commas">
|
|
<button class="btn btn-outline-primary" id="searchButton">
|
|
<i class="fas fa-search"></i> Search
|
|
</button>
|
|
</div>
|
|
<small class="form-text text-muted">
|
|
Example: [email protected], [email protected]
|
|
</small>
|
|
<div id="searchResults" class="table-responsive mt-3" style="display: none;">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Email</th>
|
|
<th>Name</th>
|
|
<th>Current Role</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="moderators-section">
|
|
<h3 class="sub-section-title mb-3">
|
|
<i class="fas fa-user-shield"></i>
|
|
Current Moderators
|
|
</h3>
|
|
<div class="table-responsive moderators-table-container">
|
|
<table class="table table-hover" id="moderatorsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Email</th>
|
|
<th>Name</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="admin-card">
|
|
<form id="adminForm" method="POST" action="/admin/submit" enctype="multipart/form-data">
|
|
<div class="form-section">
|
|
<h2 class="section-title">
|
|
<i class="fas fa-language"></i>
|
|
Language Selection
|
|
</h2>
|
|
<div class="language-select">
|
|
<label for="language" class="form-label">Select language for transcription</label>
|
|
<select id="language" name="language" class="form-control" required>
|
|
<option value="">Select Language</option>
|
|
{% for lang in languages %}
|
|
<option value="{{ lang.code }}">{{ lang.name }} ({{ lang.native_name }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
|
|
<div class="domain-select mt-3">
|
|
<label for="domain" class="form-label">Select domain</label>
|
|
<select id="domain" name="domain" class="form-control" required>
|
|
{% for code, name in domains.items() %}
|
|
<option value="{{ code }}" {% if code == "GEN" %}selected{% endif %}>{{ name }} ({{ code }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
|
|
<div class="subdomain-select mt-3">
|
|
<label for="subdomain" class="form-label">Select subdomain</label>
|
|
<select id="subdomain" name="subdomain" class="form-control" required disabled>
|
|
|
|
<option value="">Loading...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h2 class="section-title">
|
|
<i class="fas fa-file-upload"></i>
|
|
Upload Transcription
|
|
</h2>
|
|
<div class="file-upload-area" id="dropArea">
|
|
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
|
<h5>Drop your file here or click to browse</h5>
|
|
<p class="text-muted">Supported formats: .txt, .csv</p>
|
|
<input type="file" name="fileInput" id="fileInput" accept=".txt,.csv" style="display: none;">
|
|
<div id="uploadProgress" class="progress" style="display: none;">
|
|
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h2 class="section-title">
|
|
<i class="fas fa-edit"></i>
|
|
Preview and Edit
|
|
</h2>
|
|
<div class="stats-bar mb-2">
|
|
<span class="me-3">
|
|
<i class="fas fa-list"></i>
|
|
Lines: <span id="lineCount">0</span>
|
|
</span>
|
|
<span>
|
|
<i class="fas fa-font"></i>
|
|
Words: <span id="wordCount">0</span>
|
|
</span>
|
|
</div>
|
|
<textarea id="transcription_text" name="transcription_text"
|
|
class="form-control" rows="10" required
|
|
placeholder="Transcription text will appear here..."
|
|
onchange="updateCounts(this.value)"></textarea>
|
|
</div>
|
|
|
|
<div class="action-buttons">
|
|
<button type="submit" class="btn btn-primary btn-admin" id="submitButton">
|
|
<i class="fas fa-save"></i>
|
|
Submit
|
|
</button>
|
|
<div class="upload-loading" id="uploadLoading" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Uploading...</span>
|
|
</div>
|
|
<span class="upload-status">Uploading transcriptions...</span>
|
|
</div>
|
|
<button type="reset" class="btn btn-outline-secondary btn-admin">
|
|
<i class="fas fa-undo"></i>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
<div class="admin-card mb-4">
|
|
<h2 class="section-title mb-4">
|
|
<i class="fas fa-sync"></i>
|
|
Dataset Synchronization
|
|
</h2>
|
|
<div class="sync-controls">
|
|
<div class="sync-info">
|
|
<div class="alert alert-info mb-3">
|
|
<i class="fas fa-info-circle"></i>
|
|
<div class="alert-content">
|
|
<h3 class="alert-heading">About Dataset Sync</h3>
|
|
<p class="mb-0">This will synchronize all verified recordings and their metadata to Hugging Face. The process includes:</p>
|
|
<ul class="sync-details">
|
|
<li>Updating Parquet metadata files</li>
|
|
<li>Syncing verified audio recordings</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sync-actions">
|
|
<button id="syncButton" class="btn btn-primary">
|
|
<i class="fas fa-sync"></i>
|
|
Start Synchronization
|
|
</button>
|
|
<div id="syncStatus" class="mt-3" style="display: none;">
|
|
<div class="sync-progress">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Syncing...</span>
|
|
</div>
|
|
<div class="sync-status-text">
|
|
<span class="status-message">Synchronizing dataset...</span>
|
|
<span class="status-detail">This may take several minutes</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const fileInput = document.getElementById('fileInput');
|
|
const transcriptionText = document.getElementById('transcription_text');
|
|
const adminForm = document.getElementById('adminForm');
|
|
const uploadProgress = document.getElementById('uploadProgress');
|
|
const dropArea = document.getElementById('dropArea');
|
|
const domainSelect = document.getElementById('domain');
|
|
const subdomainSelect = document.getElementById('subdomain');
|
|
|
|
|
|
async function loadSubdomains(domainCode) {
|
|
subdomainSelect.innerHTML = '<option value="">Loading...</option>';
|
|
subdomainSelect.disabled = true;
|
|
|
|
if (!domainCode) {
|
|
|
|
domainSelect.value = 'GEN';
|
|
domainCode = 'GEN';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/admin/subdomains/${domainCode}`);
|
|
const data = await response.json();
|
|
|
|
if (data.subdomains && Array.isArray(data.subdomains)) {
|
|
|
|
subdomainSelect.innerHTML = '';
|
|
|
|
data.subdomains.forEach(subdomain => {
|
|
const option = document.createElement('option');
|
|
option.value = subdomain.mnemonic;
|
|
option.textContent = `${subdomain.name} (${subdomain.mnemonic})`;
|
|
|
|
|
|
if (subdomain.mnemonic === 'GEN') {
|
|
option.selected = true;
|
|
}
|
|
|
|
subdomainSelect.appendChild(option);
|
|
});
|
|
|
|
subdomainSelect.disabled = false;
|
|
|
|
|
|
if (!subdomainSelect.value && subdomainSelect.options.length > 0) {
|
|
subdomainSelect.options[0].selected = true;
|
|
}
|
|
} else {
|
|
subdomainSelect.innerHTML = '<option value="">No subdomains available</option>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching subdomains:', error);
|
|
subdomainSelect.innerHTML = '<option value="">Error loading subdomains</option>';
|
|
}
|
|
}
|
|
|
|
|
|
domainSelect.addEventListener('change', function() {
|
|
loadSubdomains(this.value);
|
|
});
|
|
|
|
|
|
dropArea.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
function updateCounts(text) {
|
|
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
const words = text.trim().split(/\s+/).filter(word => word.length > 0);
|
|
|
|
document.getElementById('lineCount').textContent = lines.length;
|
|
document.getElementById('wordCount').textContent = words.length;
|
|
}
|
|
|
|
fileInput.addEventListener('change', function() {
|
|
const file = this.files[0];
|
|
if(file) {
|
|
handleFileUpload(file);
|
|
}
|
|
});
|
|
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
function highlight(e) {
|
|
dropArea.classList.add('drag-highlight');
|
|
}
|
|
|
|
function unhighlight(e) {
|
|
dropArea.classList.remove('drag-highlight');
|
|
}
|
|
|
|
dropArea.addEventListener('drop', handleDrop, false);
|
|
|
|
function handleDrop(e) {
|
|
const dt = e.dataTransfer;
|
|
const file = dt.files[0];
|
|
|
|
if (file) {
|
|
const fileType = file.name.split('.').pop().toLowerCase();
|
|
if (['txt', 'csv'].includes(fileType)) {
|
|
fileInput.files = dt.files;
|
|
handleFileUpload(file);
|
|
} else {
|
|
alert('Please upload only .txt or .csv files');
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleFileUpload(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const content = e.target.result;
|
|
transcriptionText.value = content;
|
|
updateCounts(content);
|
|
uploadProgress.style.display = 'block';
|
|
uploadProgress.querySelector('.progress-bar').style.width = '100%';
|
|
}
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
|
|
transcriptionText.addEventListener('input', function() {
|
|
updateCounts(this.value);
|
|
});
|
|
|
|
adminForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const submitButton = document.getElementById('submitButton');
|
|
const uploadLoading = document.getElementById('uploadLoading');
|
|
|
|
|
|
submitButton.disabled = true;
|
|
uploadLoading.style.display = 'flex';
|
|
|
|
|
|
const formData = new FormData();
|
|
formData.append('language', document.getElementById('language').value);
|
|
formData.append('domain', document.getElementById('domain').value);
|
|
formData.append('subdomain', document.getElementById('subdomain').value);
|
|
formData.append('transcription_text', transcriptionText.value);
|
|
|
|
|
|
const fileInputElement = document.getElementById('fileInput');
|
|
if (fileInputElement.files.length > 0) {
|
|
formData.append('fileInput', fileInputElement.files[0]);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/admin/submit', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
if(response.ok) {
|
|
alert(`Transcriptions uploaded successfully!\nLines: ${document.getElementById('lineCount').textContent}\nWords: ${document.getElementById('wordCount').textContent}`);
|
|
uploadProgress.style.display = 'none';
|
|
transcriptionText.value = '';
|
|
fileInput.value = '';
|
|
|
|
updateCounts('');
|
|
} else {
|
|
throw new Error(result.error || 'Upload failed');
|
|
}
|
|
} catch(error) {
|
|
alert('Error: ' + error.message);
|
|
} finally {
|
|
|
|
submitButton.disabled = false;
|
|
uploadLoading.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
async function loadModerators() {
|
|
try {
|
|
const response = await fetch('/admin/users/moderators');
|
|
const data = await response.json();
|
|
const tbody = document.querySelector('#moderatorsTable tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
if (data.users.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center">No moderators found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
data.users.forEach(user => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${user.email}</td>
|
|
<td>${user.name || 'N/A'}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-warning remove-mod" data-user-id="${user.id}">
|
|
Remove Moderator
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
|
|
document.querySelectorAll('.remove-mod').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
if(confirm('Remove moderator privileges from this user?')) {
|
|
await updateUserRole(this.dataset.userId, 'user');
|
|
loadModerators();
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading moderators:', error);
|
|
}
|
|
}
|
|
|
|
async function updateUserRole(userId, newRole) {
|
|
try {
|
|
const response = await fetch(`/admin/users/${userId}/role`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ role: newRole })
|
|
});
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (!contentType || !contentType.includes('application/json')) {
|
|
throw new Error('Server response was not JSON');
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Failed to update role');
|
|
}
|
|
|
|
await loadModerators();
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error updating role:', error);
|
|
alert('Error updating role: ' + error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
document.getElementById('searchButton').addEventListener('click', async function() {
|
|
const emailInput = document.getElementById('searchEmail').value.trim();
|
|
if (!emailInput) {
|
|
alert('Please enter at least one email address');
|
|
return;
|
|
}
|
|
|
|
|
|
const emails = emailInput.split(',').map(e => e.trim()).filter(e => e);
|
|
const validateEmail = (email) => {
|
|
return String(email)
|
|
.toLowerCase()
|
|
.match(/^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/);
|
|
};
|
|
|
|
const invalidEmails = emails.filter(email => !validateEmail(email));
|
|
if (invalidEmails.length > 0) {
|
|
alert(`Invalid email address(es):\n${invalidEmails.join('\n')}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/admin/users/search?email=${encodeURIComponent(emailInput)}`);
|
|
const data = await response.json();
|
|
|
|
const resultsDiv = document.getElementById('searchResults');
|
|
const tbody = resultsDiv.querySelector('tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
if (data.users.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center">No users found</td></tr>';
|
|
} else {
|
|
data.users.forEach(user => {
|
|
const tr = document.createElement('tr');
|
|
const isModerator = user.role === 'moderator';
|
|
const isAdmin = user.role === 'admin';
|
|
|
|
tr.innerHTML = `
|
|
<td>${user.email}</td>
|
|
<td>${user.name || 'N/A'}</td>
|
|
<td>${user.role}</td>
|
|
<td>
|
|
${isAdmin ?
|
|
'<span class="text-muted">Admin - No Action Available</span>' :
|
|
`<button class="btn btn-sm ${isModerator ? 'btn-warning' : 'btn-primary'} toggle-role"
|
|
data-user-id="${user.id}"
|
|
data-current-role="${user.role}">
|
|
${isModerator ? 'Remove Moderator' : 'Make Moderator'}
|
|
</button>`
|
|
}
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
resultsDiv.style.display = 'block';
|
|
|
|
|
|
document.querySelectorAll('.toggle-role').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const userId = this.dataset.userId;
|
|
const currentRole = this.dataset.currentRole;
|
|
const newRole = currentRole === 'moderator' ? 'user' : 'moderator';
|
|
const action = newRole === 'moderator' ? 'make this user a moderator' : 'remove moderator privileges';
|
|
|
|
if(confirm(`Are you sure you want to ${action}?`)) {
|
|
await updateUserRole(userId, newRole);
|
|
loadModerators();
|
|
|
|
document.getElementById('searchButton').click();
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error searching users:', error);
|
|
alert('Error searching users');
|
|
}
|
|
});
|
|
|
|
|
|
document.querySelectorAll('.make-mod').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
if (confirm('Make this user a moderator?')) {
|
|
const success = await updateUserRole(this.dataset.userId, 'moderator');
|
|
if (success) {
|
|
document.getElementById('searchEmail').value = '';
|
|
document.getElementById('searchResults').style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.remove-mod').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
if (confirm('Remove moderator privileges from this user?')) {
|
|
await updateUserRole(this.dataset.userId, 'user');
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
loadModerators();
|
|
|
|
|
|
if (domainSelect.value) {
|
|
await loadSubdomains(domainSelect.value);
|
|
} else {
|
|
|
|
domainSelect.value = 'GEN';
|
|
await loadSubdomains('GEN');
|
|
}
|
|
});
|
|
|
|
|
|
document.getElementById('syncButton').addEventListener('click', async function() {
|
|
try {
|
|
const button = this;
|
|
const statusDiv = document.getElementById('syncStatus');
|
|
|
|
|
|
const statusResponse = await fetch('/admin/sync/status');
|
|
const statusData = await statusResponse.json();
|
|
|
|
if (statusData.is_syncing) {
|
|
alert('A sync operation is already in progress');
|
|
return;
|
|
}
|
|
|
|
|
|
button.disabled = true;
|
|
statusDiv.style.display = 'block';
|
|
|
|
const response = await fetch('/admin/sync', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
alert('Dataset synchronization completed successfully!');
|
|
} else if (response.status === 409) {
|
|
alert('A sync operation is already in progress');
|
|
} else {
|
|
throw new Error(result.error || 'Sync failed');
|
|
}
|
|
} catch (error) {
|
|
alert('Error: ' + error.message);
|
|
} finally {
|
|
|
|
this.disabled = false;
|
|
document.getElementById('syncStatus').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|