feat: isolate profanity filter with separate database

- Create dedicated ProfanityFilter class with isolated SQLite database
- Separate profanity.db from main application database to prevent SQLITE_MISUSE errors
- Add comprehensive custom word management (CRUD operations)
- Implement advanced profanity detection with leetspeak and pattern matching
- Add admin UI for managing custom profanity words
- Add extensive test suites for both profanity filter and API routes
- Update server.js to use isolated profanity filter
- Add proper database initialization and cleanup methods
- Support in-memory databases for testing

Breaking changes:
- Profanity filter now uses separate database file
- Updated admin API endpoints for profanity management
- Enhanced profanity detection capabilities
This commit is contained in:
Deco Vander 2025-07-04 00:03:24 -04:00
parent d19cd2766c
commit c7f39e4939
15 changed files with 5365 additions and 257 deletions

View file

@ -319,7 +319,10 @@
<div class="admin-container">
<!-- Login Section -->
<div id="login-section" class="login-section">
<h2>🔐 Admin Login</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">🔐 Admin Login</h2>
<a href="/" class="header-btn btn-home" style="text-decoration: none; font-size: 12px; padding: 8px 12px;">🏠 Back to Homepage</a>
</div>
<form id="login-form">
<div class="form-group">
<label for="password">Admin Password:</label>
@ -387,6 +390,6 @@
</div>
</div>
<script src="https://iceymi.b-cdn.net/admin.js"></script>
<script src="admin.js"></script>
</body>
</html>

View file

@ -453,7 +453,14 @@ document.addEventListener('DOMContentLoaded', async () => {
}),
});
})
.then(res => res.json())
.then(res => {
if (!res.ok) {
return res.json().then(errorData => {
throw { status: res.status, data: errorData };
});
}
return res.json();
})
.then(location => {
refreshLocations();
messageDiv.textContent = 'Location reported successfully!';
@ -462,17 +469,34 @@ document.addEventListener('DOMContentLoaded', async () => {
})
.catch(err => {
console.error('Error reporting location:', err);
messageDiv.textContent = 'Error reporting location.';
messageDiv.className = 'message error';
// Handle specific profanity rejection
if (err.status === 400 && err.data && err.data.error === 'Submission rejected') {
messageDiv.textContent = err.data.message;
messageDiv.className = 'message error profanity-rejection';
// Clear the description field to encourage rewriting
document.getElementById('description').value = '';
document.getElementById('description').focus();
} else if (err.data && err.data.error) {
messageDiv.textContent = err.data.error;
messageDiv.className = 'message error';
} else {
messageDiv.textContent = 'Error reporting location. Please try again.';
messageDiv.className = 'message error';
}
})
.finally(() => {
submitBtn.disabled = false;
submitText.style.display = 'inline';
submitLoading.style.display = 'none';
messageDiv.style.display = 'block';
// Longer timeout for profanity rejection messages
const timeout = messageDiv.className.includes('profanity-rejection') ? 15000 : 3000;
setTimeout(() => {
messageDiv.style.display = 'none';
}, 3000);
}, timeout);
});
});

View file

@ -42,6 +42,7 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
<label for="description">Additional Details (Optional)</label>
<textarea id="description" name="description" rows="3"
placeholder="Number of vehicles, time observed, etc."></textarea>
<small class="input-help">Keep descriptions appropriate and relevant to road conditions. Submissions with inappropriate language will be rejected.</small>
</div>
<button type="submit" id="submit-btn">

View file

@ -182,6 +182,7 @@ input[type="text"], textarea {
display: block;
}
button[type="submit"] {
background-color: var(--button-bg);
color: white;
@ -213,6 +214,26 @@ button[type="submit"]:hover {
color: #721c24;
}
.message.error.profanity-rejection {
background-color: #f5c6cb;
color: #721c24;
border: 2px solid #dc3545;
font-weight: bold;
animation: pulse 0.5s ease-in-out;
}
[data-theme="dark"] .message.error.profanity-rejection {
background-color: #3d1b1c;
border-color: #dc3545;
color: #f5c6cb;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
/* Header layout for theme toggle */
.header-content {
display: flex;