Major refactor: Fix SQLite concurrency, remove rate limiting, simplify architecture
- Switch to single Gunicorn worker to eliminate SQLite database locking issues - Remove Flask-Limiter and all rate limiting complexity - Remove Cloudflare proxy setup and dependencies - Simplify configuration and remove unnecessary features - Update all templates and static files for streamlined operation - Clean up old files and documentation - Restore stable database from backup - System now runs fast and reliably without database locks
This commit is contained in:
189
static/modapp.js
Normal file
189
static/modapp.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* ModApp JavaScript - AJAX moderation actions without page refresh
|
||||
*/
|
||||
|
||||
// Handle individual moderation actions (approve, reject, delete, clear_flags)
|
||||
async function moderationAction(action, quoteId, element) {
|
||||
try {
|
||||
// Show loading state
|
||||
const originalText = element.textContent;
|
||||
element.textContent = 'Loading...';
|
||||
element.style.pointerEvents = 'none';
|
||||
|
||||
const response = await fetch(`/${action}/${quoteId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest' // Tell server this is AJAX
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Show success message briefly
|
||||
showMessage(result.message, 'success');
|
||||
|
||||
// Remove the quote from the current view or update its display
|
||||
const quoteRow = element.closest('tr');
|
||||
if (quoteRow) {
|
||||
// Fade out the quote
|
||||
quoteRow.style.transition = 'opacity 0.3s ease';
|
||||
quoteRow.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
quoteRow.remove();
|
||||
updateCounters();
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
// Show error message
|
||||
showMessage(result.message || 'Action failed', 'error');
|
||||
// Restore original state
|
||||
element.textContent = originalText;
|
||||
element.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Moderation action failed:', error);
|
||||
showMessage('Network error. Please try again.', 'error');
|
||||
// Restore original state
|
||||
element.textContent = originalText;
|
||||
element.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
return false; // Prevent default link behavior
|
||||
}
|
||||
|
||||
// Show temporary message to user
|
||||
function showMessage(message, type = 'info') {
|
||||
// Remove any existing messages
|
||||
const existingMsg = document.getElementById('temp-message');
|
||||
if (existingMsg) {
|
||||
existingMsg.remove();
|
||||
}
|
||||
|
||||
// Create new message element
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.id = 'temp-message';
|
||||
msgDiv.textContent = message;
|
||||
msgDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
transition: opacity 0.3s ease;
|
||||
${type === 'success' ? 'background-color: #28a745;' : ''}
|
||||
${type === 'error' ? 'background-color: #dc3545;' : ''}
|
||||
${type === 'info' ? 'background-color: #17a2b8;' : ''}
|
||||
`;
|
||||
|
||||
document.body.appendChild(msgDiv);
|
||||
|
||||
// Auto-remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
msgDiv.style.opacity = '0';
|
||||
setTimeout(() => msgDiv.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Update quote counters (simplified - could be enhanced with actual counts)
|
||||
function updateCounters() {
|
||||
// This could be enhanced to fetch actual counts from server
|
||||
// For now, just indicate that counts may have changed
|
||||
const statusElements = document.querySelectorAll('.quote-status');
|
||||
statusElements.forEach(el => {
|
||||
el.style.opacity = '0.8';
|
||||
setTimeout(() => el.style.opacity = '1', 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle bulk actions form
|
||||
async function handleBulkAction(form, event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const action = formData.get('action');
|
||||
const quoteIds = formData.getAll('quote_ids');
|
||||
|
||||
if (quoteIds.length === 0) {
|
||||
showMessage('Please select at least one quote', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to ${action} ${quoteIds.length} quote(s)?`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/modapp/bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showMessage(result.message, 'success');
|
||||
|
||||
// Remove processed quotes from view
|
||||
quoteIds.forEach(quoteId => {
|
||||
const checkbox = document.querySelector(`input[value="${quoteId}"]`);
|
||||
if (checkbox) {
|
||||
const quoteRow = checkbox.closest('tr');
|
||||
if (quoteRow) {
|
||||
quoteRow.style.transition = 'opacity 0.3s ease';
|
||||
quoteRow.style.opacity = '0';
|
||||
setTimeout(() => quoteRow.remove(), 300);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reset form
|
||||
form.reset();
|
||||
updateCounters();
|
||||
|
||||
} else {
|
||||
showMessage(result.message || 'Bulk action failed', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Bulk action failed:', error);
|
||||
showMessage('Network error. Please try again.', 'error');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Convert all moderation links to AJAX
|
||||
document.querySelectorAll('a[href^="/approve/"], a[href^="/reject/"], a[href^="/delete/"], a[href^="/clear_flags/"]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const href = this.getAttribute('href');
|
||||
const parts = href.split('/');
|
||||
const action = parts[1]; // approve, reject, delete, clear_flags
|
||||
const quoteId = parts[2];
|
||||
|
||||
moderationAction(action, quoteId, this);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert bulk form to AJAX
|
||||
const bulkForm = document.querySelector('form[action="/modapp/bulk"]');
|
||||
if (bulkForm) {
|
||||
bulkForm.addEventListener('submit', function(e) {
|
||||
handleBulkAction(this, e);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -116,6 +116,9 @@ input.button:hover {
|
||||
display: inline-block;
|
||||
min-width: 12px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.qa:hover {
|
||||
@@ -129,6 +132,13 @@ input.button:hover {
|
||||
border: 1px inset #808080;
|
||||
}
|
||||
|
||||
/* Quote controls wrapper for better mobile layout */
|
||||
.quote-controls {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.quote {
|
||||
font-family: 'Courier New', 'Lucida Console', monospace;
|
||||
font-size: smaller;
|
||||
@@ -187,10 +197,14 @@ footer {
|
||||
color: #c08000;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 2px 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.quote-id:hover {
|
||||
text-decoration: underline;
|
||||
background-color: rgba(192, 128, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Dark Mode Toggle Button */
|
||||
@@ -415,14 +429,17 @@ html.dark-theme font {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Quote buttons - make them touch-friendly */
|
||||
/* Quote buttons - compact but touch-friendly */
|
||||
.qa {
|
||||
padding: 8px 12px;
|
||||
margin: 2px;
|
||||
min-width: 35px;
|
||||
font-size: 16px;
|
||||
padding: 6px 8px;
|
||||
margin: 1px 2px;
|
||||
min-width: 28px;
|
||||
min-height: 32px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Quote text - ensure readability */
|
||||
@@ -430,40 +447,43 @@ html.dark-theme font {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Quote header - adjust spacing */
|
||||
/* Quote header - adjust spacing and make more compact */
|
||||
.quote {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Form elements - make touch-friendly */
|
||||
input[type="text"], input[type="number"], textarea, select {
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
margin: 5px 0;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
input[type="submit"], button {
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
margin: 5px;
|
||||
min-height: 44px; /* iOS touch target */
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
margin: 3px;
|
||||
min-height: 40px; /* Reasonable touch target */
|
||||
}
|
||||
|
||||
/* Pagination - mobile friendly */
|
||||
#pagination {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#pagination a {
|
||||
padding: 8px 12px;
|
||||
padding: 6px 10px;
|
||||
margin: 2px;
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ModApp table - horizontal scroll for wide tables */
|
||||
@@ -477,24 +497,35 @@ html.dark-theme font {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Hide less important columns on mobile */
|
||||
/* Compact view for smaller screens */
|
||||
@media screen and (max-width: 480px) {
|
||||
.mobile-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.qa {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
min-width: 30px;
|
||||
padding: 5px 7px;
|
||||
font-size: 13px;
|
||||
min-width: 26px;
|
||||
min-height: 30px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.quote {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.qt {
|
||||
font-size: 13px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* Stack quote controls for very small screens */
|
||||
.quote-controls {
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,22 +534,30 @@ html.dark-theme font {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
/* This targets touch devices */
|
||||
.qa {
|
||||
padding: 10px 15px;
|
||||
margin: 3px;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 7px 10px;
|
||||
margin: 2px;
|
||||
min-height: 36px;
|
||||
min-width: 32px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 5px;
|
||||
margin: 2px;
|
||||
padding: 3px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
/* Ensure all interactive elements are large enough */
|
||||
/* Ensure all interactive elements are large enough but not oversized */
|
||||
button, input[type="submit"], input[type="button"] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 10px;
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* Theme toggle button - keep compact */
|
||||
#theme-toggle {
|
||||
min-height: 36px;
|
||||
min-width: 36px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// AJAX voting functionality
|
||||
function vote(quoteId, action, buttonElement) {
|
||||
// Prevent multiple clicks
|
||||
// Prevent multiple clicks on the same button
|
||||
if (buttonElement.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable button temporarily
|
||||
// Disable button temporarily to prevent double-clicks
|
||||
buttonElement.disabled = true;
|
||||
|
||||
// Make AJAX request
|
||||
@@ -15,9 +15,16 @@ function vote(quoteId, action, buttonElement) {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
.then(response => {
|
||||
// Handle both successful and error responses with JSON
|
||||
return response.json().then(data => {
|
||||
return { data, status: response.status, ok: response.ok };
|
||||
});
|
||||
})
|
||||
.then(result => {
|
||||
const { data, status, ok } = result;
|
||||
|
||||
if (ok && data.success) {
|
||||
// Update vote count display
|
||||
const voteElement = document.getElementById(`votes-${quoteId}`);
|
||||
if (voteElement) {
|
||||
@@ -27,7 +34,15 @@ function vote(quoteId, action, buttonElement) {
|
||||
// Update button states based on user's voting history
|
||||
updateButtonStates(quoteId, data.user_vote);
|
||||
} else {
|
||||
alert(data.message || 'Sorry, your vote could not be recorded. Please try again.');
|
||||
// Show the server's error message, with special handling for rate limiting
|
||||
let errorMessage = data.message || 'Sorry, your vote could not be recorded. Please try again.';
|
||||
|
||||
if (status === 429) {
|
||||
// Rate limiting or flood control
|
||||
errorMessage = data.message || 'Please slow down! You\'re voting too quickly. Wait a moment and try again.';
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -35,7 +50,7 @@ function vote(quoteId, action, buttonElement) {
|
||||
alert('Connection error while voting. Please check your internet connection and try again.');
|
||||
})
|
||||
.finally(() => {
|
||||
// Re-enable button
|
||||
// Re-enable the button immediately
|
||||
buttonElement.disabled = false;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user