Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
{% extends "admin/base.html" %} | |
{% block admin_content %} | |
<div class="admin-header"> | |
<div class="admin-title">User Timeout Management</div> | |
</div> | |
<!-- Create Timeout Form --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Create New Timeout</div> | |
</div> | |
<form method="POST" action="{{ url_for('admin.create_timeout') }}" class="admin-form"> | |
<div class="form-group"> | |
<label for="user_search">Search User</label> | |
<input type="text" id="user_search" class="form-control" placeholder="Type username to search..." autocomplete="off"> | |
<div id="user_search_results" class="user-search-results"></div> | |
<input type="hidden" id="user_id" name="user_id" required> | |
<div id="selected_user" class="selected-user"></div> | |
</div> | |
<div class="form-group"> | |
<label for="reason">Reason for Timeout</label> | |
<textarea id="reason" name="reason" class="form-control" rows="3" required placeholder="Explain why this user is being timed out..."></textarea> | |
</div> | |
<div class="form-group"> | |
<label for="timeout_type">Timeout Type</label> | |
<select id="timeout_type" name="timeout_type" class="form-control" required> | |
<option value="manual">Manual Admin Action</option> | |
<option value="coordinated_voting">Coordinated Voting</option> | |
<option value="rapid_voting">Rapid Voting</option> | |
<option value="security_violation">Security Violation</option> | |
<option value="spam">Spam/Abuse</option> | |
<option value="other">Other</option> | |
</select> | |
</div> | |
<div class="form-group"> | |
<label for="duration_days">Duration (Days)</label> | |
<select id="duration_days" name="duration_days" class="form-control" required> | |
<option value="1">1 Day</option> | |
<option value="3">3 Days</option> | |
<option value="7">1 Week</option> | |
<option value="14">2 Weeks</option> | |
<option value="30" selected>30 Days (Default)</option> | |
<option value="60">60 Days</option> | |
<option value="90">90 Days</option> | |
<option value="180">180 Days</option> | |
<option value="365">1 Year</option> | |
</select> | |
</div> | |
<button type="submit" class="btn-primary">Create Timeout</button> | |
</form> | |
</div> | |
<!-- Active Timeouts --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Active Timeouts ({{ active_timeouts|length }})</div> | |
</div> | |
{% if active_timeouts %} | |
<div class="table-responsive"> | |
<table class="admin-table"> | |
<thead> | |
<tr> | |
<th>User</th> | |
<th>Reason</th> | |
<th>Type</th> | |
<th>Created</th> | |
<th>Expires</th> | |
<th>Remaining</th> | |
<th>Created By</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for timeout in active_timeouts %} | |
<tr> | |
<td> | |
<a href="{{ url_for('admin.user_detail', user_id=timeout.user.id) }}"> | |
{{ timeout.user.username }} | |
</a> | |
</td> | |
<td class="text-truncate" title="{{ timeout.reason }}">{{ timeout.reason }}</td> | |
<td> | |
<span class="timeout-type-badge timeout-{{ timeout.timeout_type }}"> | |
{{ timeout.timeout_type.replace('_', ' ').title() }} | |
</span> | |
</td> | |
<td>{{ timeout.created_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td>{{ timeout.expires_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td> | |
<span class="remaining-time" data-expires="{{ timeout.expires_at.isoformat() }}"> | |
Calculating... | |
</span> | |
</td> | |
<td> | |
{% if timeout.creator %} | |
{{ timeout.creator.username }} | |
{% else %} | |
System | |
{% endif %} | |
</td> | |
<td> | |
<button class="action-btn cancel-timeout-btn" data-timeout-id="{{ timeout.id }}" data-username="{{ timeout.user.username }}"> | |
Cancel | |
</button> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<p>No active timeouts.</p> | |
{% endif %} | |
</div> | |
<!-- Recent Inactive Timeouts --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Recent Expired/Cancelled Timeouts</div> | |
</div> | |
{% if recent_inactive %} | |
<div class="table-responsive"> | |
<table class="admin-table"> | |
<thead> | |
<tr> | |
<th>User</th> | |
<th>Reason</th> | |
<th>Type</th> | |
<th>Created</th> | |
<th>Expired/Cancelled</th> | |
<th>Status</th> | |
<th>Cancelled By</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for timeout in recent_inactive %} | |
<tr> | |
<td> | |
<a href="{{ url_for('admin.user_detail', user_id=timeout.user.id) }}"> | |
{{ timeout.user.username }} | |
</a> | |
</td> | |
<td class="text-truncate" title="{{ timeout.reason }}">{{ timeout.reason }}</td> | |
<td> | |
<span class="timeout-type-badge timeout-{{ timeout.timeout_type }}"> | |
{{ timeout.timeout_type.replace('_', ' ').title() }} | |
</span> | |
</td> | |
<td>{{ timeout.created_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td> | |
{% if timeout.cancelled_at %} | |
{{ timeout.cancelled_at.strftime('%Y-%m-%d %H:%M') }} | |
{% else %} | |
{{ timeout.expires_at.strftime('%Y-%m-%d %H:%M') }} | |
{% endif %} | |
</td> | |
<td> | |
{% if timeout.cancelled_at %} | |
<span class="status-badge cancelled">Cancelled</span> | |
{% else %} | |
<span class="status-badge expired">Expired</span> | |
{% endif %} | |
</td> | |
<td> | |
{% if timeout.canceller %} | |
{{ timeout.canceller.username }} | |
{% else %} | |
- | |
{% endif %} | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<p>No recent inactive timeouts.</p> | |
{% endif %} | |
</div> | |
<!-- Cancel Timeout Modal --> | |
<div id="cancelTimeoutModal" class="modal" style="display: none;"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h3>Cancel Timeout</h3> | |
<span class="modal-close">×</span> | |
</div> | |
<form method="POST" id="cancelTimeoutForm"> | |
<div class="modal-body"> | |
<p>Are you sure you want to cancel the timeout for <strong id="cancelUsername"></strong>?</p> | |
<div class="form-group"> | |
<label for="cancel_reason">Reason for Cancellation</label> | |
<textarea id="cancel_reason" name="cancel_reason" class="form-control" rows="3" required placeholder="Explain why this timeout is being cancelled..."></textarea> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn-secondary modal-close">Cancel</button> | |
<button type="submit" class="btn-primary">Confirm Cancellation</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
<style> | |
.user-search-results { | |
position: absolute; | |
background: white; | |
border: 1px solid var(--border-color); | |
border-top: none; | |
border-radius: 0 0 var(--radius) var(--radius); | |
max-height: 200px; | |
overflow-y: auto; | |
z-index: 1000; | |
display: none; | |
width: 100%; | |
} | |
.user-search-item { | |
padding: 12px; | |
cursor: pointer; | |
border-bottom: 1px solid var(--border-color); | |
} | |
.user-search-item:hover { | |
background-color: var(--secondary-color); | |
} | |
.user-search-item:last-child { | |
border-bottom: none; | |
} | |
.selected-user { | |
margin-top: 8px; | |
padding: 8px 12px; | |
background-color: var(--secondary-color); | |
border-radius: var(--radius); | |
display: none; | |
} | |
.timeout-type-badge { | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 500; | |
color: white; | |
} | |
.timeout-manual { background-color: #6c757d; } | |
.timeout-coordinated_voting { background-color: #dc3545; } | |
.timeout-rapid_voting { background-color: #fd7e14; } | |
.timeout-security_violation { background-color: #e83e8c; } | |
.timeout-spam { background-color: #6f42c1; } | |
.timeout-other { background-color: #20c997; } | |
.status-badge { | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 500; | |
color: white; | |
} | |
.status-badge.cancelled { | |
background-color: #ffc107; | |
color: black; | |
} | |
.status-badge.expired { | |
background-color: #6c757d; | |
} | |
.modal { | |
position: fixed; | |
z-index: 1000; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0,0,0,0.5); | |
} | |
.modal-content { | |
background-color: white; | |
margin: 10% auto; | |
padding: 0; | |
border-radius: var(--radius); | |
width: 90%; | |
max-width: 500px; | |
box-shadow: var(--shadow); | |
} | |
.modal-header { | |
padding: 20px; | |
border-bottom: 1px solid var(--border-color); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.modal-header h3 { | |
margin: 0; | |
} | |
.modal-close { | |
font-size: 24px; | |
cursor: pointer; | |
color: #666; | |
} | |
.modal-close:hover { | |
color: #000; | |
} | |
.modal-body { | |
padding: 20px; | |
} | |
.modal-footer { | |
padding: 20px; | |
border-top: 1px solid var(--border-color); | |
display: flex; | |
justify-content: flex-end; | |
gap: 12px; | |
} | |
.form-group { | |
position: relative; | |
} | |
</style> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// User search functionality | |
const userSearch = document.getElementById('user_search'); | |
const userSearchResults = document.getElementById('user_search_results'); | |
const userIdInput = document.getElementById('user_id'); | |
const selectedUserDiv = document.getElementById('selected_user'); | |
let searchTimeout; | |
userSearch.addEventListener('input', function() { | |
const query = this.value.trim(); | |
if (query.length < 2) { | |
userSearchResults.style.display = 'none'; | |
return; | |
} | |
clearTimeout(searchTimeout); | |
searchTimeout = setTimeout(() => { | |
fetch(`{{ url_for('admin.user_search') }}?q=${encodeURIComponent(query)}`) | |
.then(response => response.json()) | |
.then(users => { | |
userSearchResults.innerHTML = ''; | |
if (users.length === 0) { | |
userSearchResults.innerHTML = '<div class="user-search-item">No users found</div>'; | |
} else { | |
users.forEach(user => { | |
const item = document.createElement('div'); | |
item.className = 'user-search-item'; | |
item.innerHTML = `<strong>${user.username}</strong><br><small>ID: ${user.id}, Joined: ${user.join_date}</small>`; | |
item.addEventListener('click', () => selectUser(user)); | |
userSearchResults.appendChild(item); | |
}); | |
} | |
userSearchResults.style.display = 'block'; | |
}) | |
.catch(error => { | |
console.error('Error searching users:', error); | |
}); | |
}, 300); | |
}); | |
function selectUser(user) { | |
userIdInput.value = user.id; | |
userSearch.value = ''; | |
userSearchResults.style.display = 'none'; | |
selectedUserDiv.innerHTML = `<strong>Selected:</strong> ${user.username} (ID: ${user.id})`; | |
selectedUserDiv.style.display = 'block'; | |
} | |
// Hide search results when clicking outside | |
document.addEventListener('click', function(e) { | |
if (!userSearch.contains(e.target) && !userSearchResults.contains(e.target)) { | |
userSearchResults.style.display = 'none'; | |
} | |
}); | |
// Cancel timeout modal | |
const modal = document.getElementById('cancelTimeoutModal'); | |
const cancelForm = document.getElementById('cancelTimeoutForm'); | |
const cancelUsername = document.getElementById('cancelUsername'); | |
const cancelButtons = document.querySelectorAll('.cancel-timeout-btn'); | |
const modalCloseButtons = document.querySelectorAll('.modal-close'); | |
cancelButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
const timeoutId = this.dataset.timeoutId; | |
const username = this.dataset.username; | |
cancelForm.action = `{{ url_for('admin.cancel_timeout', timeout_id=0) }}`.replace('0', timeoutId); | |
cancelUsername.textContent = username; | |
modal.style.display = 'block'; | |
}); | |
}); | |
modalCloseButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
modal.style.display = 'none'; | |
}); | |
}); | |
// Close modal when clicking outside | |
window.addEventListener('click', function(e) { | |
if (e.target === modal) { | |
modal.style.display = 'none'; | |
} | |
}); | |
// Calculate remaining time for timeouts | |
function updateRemainingTimes() { | |
const remainingTimeElements = document.querySelectorAll('.remaining-time'); | |
const now = new Date(); | |
remainingTimeElements.forEach(element => { | |
const expiresAt = new Date(element.dataset.expires); | |
const remaining = expiresAt - now; | |
if (remaining <= 0) { | |
element.textContent = 'Expired'; | |
element.style.color = '#dc3545'; | |
} else { | |
const days = Math.floor(remaining / (1000 * 60 * 60 * 24)); | |
const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); | |
if (days > 0) { | |
element.textContent = `${days} day(s)`; | |
} else { | |
element.textContent = `${hours} hour(s)`; | |
} | |
} | |
}); | |
} | |
// Update remaining times initially and every minute | |
updateRemainingTimes(); | |
setInterval(updateRemainingTimes, 60000); | |
}); | |
</script> | |
{% endblock %} |