Splitting css and JS files on settings pages

This commit is contained in:
2025-06-13 14:18:35 +02:00
parent a801eb1eeb
commit f0115a70f9
12 changed files with 1385 additions and 1149 deletions

View File

@@ -0,0 +1,135 @@
/* Connection Cards */
.card {
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card-header {
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
padding: 1rem;
}
.card-body {
padding: 1.25rem;
}
/* Form Elements */
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.form-control {
border-radius: 0.375rem;
border: 1px solid #ced4da;
padding: 0.5rem 0.75rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25);
}
.form-text {
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.25rem;
}
/* Buttons */
.btn {
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-weight: 500;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: #fff;
}
/* Modal */
.modal-content {
border-radius: 0.5rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.modal-header {
border-bottom: 1px solid #dee2e6;
padding: 1rem;
}
.modal-body {
padding: 1.25rem;
}
.modal-footer {
border-top: 1px solid #dee2e6;
padding: 1rem;
}
/* Input Groups */
.input-group {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.input-group .form-control {
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
margin-bottom: 0;
}
.input-group .btn {
position: relative;
z-index: 2;
}
/* Icons */
.fas {
margin-right: 0.5rem;
}
/* Responsive */
@media (max-width: 768px) {
.card {
margin-bottom: 1rem;
}
.btn {
width: 100%;
margin-bottom: 0.5rem;
}
.input-group {
flex-direction: column;
}
.input-group .btn {
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,43 @@
/* Summernote custom styles */
.note-editor {
margin-bottom: 0;
}
.note-editor.note-frame {
border-color: #dee2e6;
border-radius: 0.375rem;
}
.note-editor.note-frame:focus-within {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.note-toolbar {
background-color: #f8f9fa;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
border-bottom: 1px solid #dee2e6;
}
.note-editing-area {
background-color: #fff;
}
.note-statusbar {
border-top: 1px solid #dee2e6;
}
.note-placeholder {
color: #6c757d;
}
/* Variable card styles */
#variableList .card {
border: 1px solid #dee2e6;
transition: all 0.2s ease-in-out;
}
#variableList .card:hover {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1);
}
#variableList code {
font-size: 0.9em;
padding: 0.2em 0.4em;
background-color: #f8f9fa;
border-radius: 0.25rem;
}

View File

@@ -0,0 +1,95 @@
/* Log filters form */
#logFiltersForm .form-label {
font-weight: 500;
color: #495057;
}
#logFiltersForm .form-control,
#logFiltersForm .form-select {
border-color: #dee2e6;
border-radius: 0.375rem;
}
#logFiltersForm .form-control:focus,
#logFiltersForm .form-select:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Logs table */
.table {
margin-bottom: 0;
}
.table th {
font-weight: 600;
color: #495057;
border-bottom-width: 1px;
}
.table td {
vertical-align: middle;
}
/* Log level badges */
.badge {
font-weight: 500;
padding: 0.35em 0.65em;
}
.badge.bg-info {
background-color: #0dcaf0 !important;
}
.badge.bg-warning {
background-color: #ffc107 !important;
color: #000;
}
.badge.bg-danger {
background-color: #dc3545 !important;
}
.badge.bg-secondary {
background-color: #6c757d !important;
}
/* Log details modal */
#logDetailsContent {
max-height: 70vh;
overflow-y: auto;
}
#logDetailsContent .bg-light {
background-color: #f8f9fa !important;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
}
/* Pagination */
.pagination {
margin-bottom: 0;
}
.page-link {
color: #0d6efd;
border-color: #dee2e6;
}
.page-link:hover {
color: #0a58ca;
background-color: #e9ecef;
border-color: #dee2e6;
}
.page-item.active .page-link {
background-color: #0d6efd;
border-color: #0d6efd;
}
.page-item.disabled .page-link {
color: #6c757d;
pointer-events: none;
background-color: #fff;
border-color: #dee2e6;
}

View File

@@ -0,0 +1,105 @@
/* SMTP Settings Form */
#smtpSettingsForm .form-label {
font-weight: 500;
color: #495057;
}
#smtpSettingsForm .form-control,
#smtpSettingsForm .form-select {
border-color: #dee2e6;
border-radius: 0.375rem;
}
#smtpSettingsForm .form-control:focus,
#smtpSettingsForm .form-select:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Info Icons */
.fa-info-circle {
color: var(--secondary-color);
cursor: pointer;
transition: color 0.2s ease-in-out;
}
.fa-info-circle:hover {
color: var(--primary-color);
}
/* Popover Customization */
.popover {
max-width: 400px;
border: 1px solid #dee2e6;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.popover .popover-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
.popover .popover-body {
padding: 1rem;
color: #495057;
}
/* Alert Styling */
.alert {
border: none;
border-radius: 0.375rem;
}
.alert-info {
background-color: #e7f5ff;
color: #0c5460;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
}
/* Code Blocks in Alerts */
.alert code {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.9em;
}
/* Test Connection Result */
#testConnectionResult {
margin-top: 1rem;
}
#testConnectionResult .alert {
margin-bottom: 0;
}
/* Button Styling */
.btn-outline-primary {
border-color: #0d6efd;
color: #0d6efd;
}
.btn-outline-primary:hover {
background-color: #0d6efd;
color: #fff;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
}

View File

@@ -0,0 +1,403 @@
// Form validation
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms).forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
// Show success modal
function showSuccess(message) {
const successModal = document.getElementById('successModal');
if (successModal) {
document.getElementById('successMessage').textContent = message;
new bootstrap.Modal(successModal).show();
} else {
alert(message); // Fallback if modal doesn't exist
}
}
// Show error modal
function showError(message) {
const errorModal = document.getElementById('errorModal');
if (errorModal) {
document.getElementById('errorMessage').textContent = message;
new bootstrap.Modal(errorModal).show();
} else {
alert(message); // Fallback if modal doesn't exist
}
}
// Get CSRF token from meta tag
function getCsrfToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : '';
}
// Get JWT token using management API key
async function getJwtToken() {
try {
const response = await fetch('/api/admin/management-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': document.querySelector('meta[name="management-api-key"]').getAttribute('content')
}
});
if (!response.ok) {
throw new Error('Failed to get JWT token');
}
const data = await response.json();
return data.token;
} catch (error) {
console.error('Error getting JWT token:', error);
throw error;
}
}
// Load Gitea Repositories
async function loadGiteaRepos() {
const url = document.getElementById('giteaUrl').value;
const token = document.getElementById('giteaToken').value;
const repoSelect = document.getElementById('giteaRepo');
const currentRepo = repoSelect.value; // Store current selection
if (!url || !token) {
showError('Please fill in the server URL and access token');
return;
}
try {
const response = await fetch('/api/admin/list-gitea-repos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ url, token })
});
const data = await response.json();
if (response.ok) {
repoSelect.innerHTML = '<option value="">Select a repository</option>';
data.repositories.forEach(repo => {
const option = document.createElement('option');
option.value = repo.full_name;
option.textContent = repo.full_name;
if (repo.full_name === currentRepo) { // Restore selection
option.selected = true;
}
repoSelect.appendChild(option);
});
} else {
throw new Error(data.message || 'Failed to load repositories');
}
} catch (error) {
showError(error.message);
}
}
// Test Git Connection
async function testGitConnection(provider) {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const url = document.getElementById('giteaUrl').value;
const username = document.getElementById('giteaUsername').value;
const token = document.getElementById('giteaToken').value;
if (!url || !username || !token) {
throw new Error('Please fill in all required fields');
}
const data = {
provider: 'gitea',
url: url,
username: username,
token: token
};
const response = await fetch('/settings/test-git-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Connection test failed');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Test Portainer Connection
async function testPortainerConnection() {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const url = document.getElementById('portainerUrl').value;
const apiKey = document.getElementById('portainerApiKey').value;
if (!url || !apiKey) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/api/admin/test-portainer-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
api_key: apiKey
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Connection test failed');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Test NGINX Connection
async function testNginxConnection() {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const url = document.getElementById('nginxUrl').value;
const username = document.getElementById('nginxUsername').value;
const password = document.getElementById('nginxPassword').value;
if (!url || !username || !password) {
throw new Error('Please fill in all required fields');
}
// First, get the token
const tokenResponse = await fetch(`${url}/api/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
identity: username,
secret: password
})
});
if (!tokenResponse.ok) {
throw new Error('Failed to authenticate with NGINX Proxy Manager');
}
const tokenData = await tokenResponse.json();
const token = tokenData.token;
// Now test the connection using the token
const response = await fetch(`${url}/api/nginx/proxy-hosts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to connect to NGINX Proxy Manager');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Save Portainer Connection
async function savePortainerConnection(event) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const url = document.getElementById('portainerUrl').value;
const apiKey = document.getElementById('portainerApiKey').value;
if (!url || !apiKey) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/settings/save-portainer-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
api_key: apiKey
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Save NGINX Connection
async function saveNginxConnection(event) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const url = document.getElementById('nginxUrl').value;
const username = document.getElementById('nginxUsername').value;
const password = document.getElementById('nginxPassword').value;
if (!url || !username || !password) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/settings/save-nginx-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
username: username,
password: password
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Save Git Connection
async function saveGitConnection(event, provider) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const url = document.getElementById('giteaUrl').value;
const username = document.getElementById('giteaUsername').value;
const token = document.getElementById('giteaToken').value;
const repo = document.getElementById('giteaRepo').value;
const password = document.getElementById('giteaPassword').value;
const otp = document.getElementById('giteaOtp').value;
if (!url || !username || !token || !repo) {
throw new Error('Please fill in all required fields');
}
const data = {
provider: 'gitea',
url: url,
username: username,
token: token,
repo: repo,
password: password,
otp: otp
};
const response = await fetch('/settings/save-git-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
const gitSettings = JSON.parse(document.querySelector('meta[name="git-settings"]').getAttribute('content'));
if (gitSettings && gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) {
loadGiteaRepos();
}
});

View File

@@ -0,0 +1,258 @@
// Template variables mapping
const templateVariables = {
'Account Created': {
'user.username': 'The username of the created account',
'user.email': 'The email address of the created account',
'user.company': 'The company name of the user',
'user.position': 'The position of the user in their company',
'created_at': 'The date and time when the account was created',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL',
'setup_link': 'The link to set up the user\'s password (expires in 24 hours)',
'created_by': 'The name of the admin who created the account'
},
'Password Reset': {
'user.username': 'The username of the account',
'user.email': 'The email address of the account',
'reset_link': 'The password reset link',
'expiry_time': 'The time when the reset link expires',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Account Deleted': {
'user.username': 'The username of the deleted account',
'user.email': 'The email address of the deleted account',
'deleted_at': 'The date and time of deletion',
'deleted_by': 'The username of the admin who deleted the account',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Account Updated': {
'user.username': 'The username of the updated account',
'user.email': 'The email address of the account',
'updated_fields': 'The fields that were updated',
'updated_at': 'The date and time of the update',
'updated_by': 'The username of who made the update',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Room Invite': {
'user.username': 'The username of the invited user',
'user.email': 'The email address of the invited user',
'room.name': 'The name of the room',
'room.description': 'The description of the room',
'inviter.username': 'The username of the user who sent the invite',
'inviter.email': 'The email address of the inviter',
'invite_link': 'The link to accept the room invite',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Room Invite Removed': {
'user.username': 'The username of the user',
'user.email': 'The email address of the user',
'room.name': 'The name of the room',
'room.description': 'The description of the room',
'remover.username': 'The username of the user who removed the invite',
'remover.email': 'The email address of the remover',
'removed_at': 'The date and time when the invite was removed',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Conversation Invite': {
'user.username': 'The username of the invited user',
'user.email': 'The email address of the invited user',
'conversation.name': 'The name of the conversation',
'conversation.description': 'The description of the conversation',
'inviter.username': 'The username of the user who sent the invite',
'inviter.email': 'The email address of the inviter',
'invite_link': 'The link to accept the conversation invite',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Conversation Invite Removed': {
'user.username': 'The username of the user',
'user.email': 'The email address of the user',
'conversation.name': 'The name of the conversation',
'conversation.description': 'The description of the conversation',
'remover.username': 'The username of the user who removed the invite',
'remover.email': 'The email address of the remover',
'removed_at': 'The date and time when the invite was removed',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
},
'Conversation Message': {
'user.username': 'The username of the recipient',
'user.email': 'The email address of the recipient',
'sender.username': 'The username of the message sender',
'sender.email': 'The email address of the sender',
'message.content': 'The content of the message',
'message.created_at': 'The date and time when the message was sent',
'conversation.name': 'The name of the conversation',
'conversation.description': 'The description of the conversation',
'message_link': 'The link to view the message',
'site.company_name': 'The name of your company',
'site.company_website': 'Your company website URL'
}
};
// Function to update available variables display
function updateAvailableVariables(templateType) {
const variables = templateVariables[templateType] || {};
const variableList = document.getElementById('variableList');
const availableVariables = document.getElementById('availableVariables');
if (Object.keys(variables).length === 0) {
availableVariables.style.display = 'none';
return;
}
let html = '<div class="row">';
for (const [variable, description] of Object.entries(variables)) {
html += `
<div class="col-md-6 mb-2">
<div class="card h-100">
<div class="card-body p-2">
<code class="text-primary">{{ '{{ ' }}${variable}{{ ' }}' }}</code>
<p class="text-muted small mb-0">${description}</p>
</div>
</div>
</div>
`;
}
html += '</div>';
variableList.innerHTML = html;
availableVariables.style.display = 'block';
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Initialize Summernote
$('#templateBody').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'italic', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
],
styleTags: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
placeholder: 'Enter your email template content here...',
callbacks: {
onImageUpload: function(files) {
// Disable image upload for email templates
return false;
}
},
popover: {
image: [],
link: [],
air: []
}
});
// Handle template selection
$('#templateSelect').on('change', function() {
const selectedOption = this.options[this.selectedIndex];
const subject = selectedOption.dataset.subject || '';
const body = selectedOption.dataset.body || '';
const templateType = selectedOption.dataset.type || '';
// Show the template editor
$('#templateEditor').show();
// Update the form fields
$('#templateSubject').val(subject);
$('#templateBody').summernote('code', body);
updateAvailableVariables(templateType);
});
// Check for initially selected template
const templateSelect = document.getElementById('templateSelect');
if (templateSelect.value) {
const selectedOption = templateSelect.options[templateSelect.selectedIndex];
const templateType = selectedOption.dataset.type || '';
updateAvailableVariables(templateType);
// Show the template editor if a template is initially selected
$('#templateEditor').show();
}
// Handle template save
$('#templateForm').on('submit', function(event) {
event.preventDefault();
const templateId = $('#templateSelect').val();
const subject = $('#templateSubject').val();
const body = $('#templateBody').summernote('code');
if (!templateId) {
alert('Please select a template first');
return;
}
// Show loading state
const saveButton = this.querySelector('button[type="submit"]');
const originalText = saveButton.innerHTML;
saveButton.disabled = true;
saveButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
// Get CSRF token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// Send AJAX request
fetch(`/settings/email-templates/${templateId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
subject: subject,
body: body
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Show success message
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show mt-3';
alert.innerHTML = `
<i class="fas fa-check-circle me-2"></i>Template saved successfully
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.querySelector('.card-body').appendChild(alert);
// Update the select option's data attributes
const option = document.getElementById('templateSelect').options[document.getElementById('templateSelect').selectedIndex];
option.dataset.subject = subject;
option.dataset.body = body;
})
.catch(error => {
// Show error message
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show mt-3';
alert.innerHTML = `
<i class="fas fa-exclamation-circle me-2"></i>${error.message || 'Failed to save template'}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.querySelector('.card-body').appendChild(alert);
})
.finally(() => {
// Restore button state
saveButton.disabled = false;
saveButton.innerHTML = originalText;
});
});
});

181
static/js/settings/logs.js Normal file
View File

@@ -0,0 +1,181 @@
// Function to view log details
function viewLogDetails(logId) {
fetch(`/api/logs/${logId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch log details');
}
return response.json();
})
.then(log => {
const content = document.getElementById('logDetailsContent');
// Format the details in a readable way
let details = log.details;
let formattedDetails = 'No details available';
if (details) {
if (typeof details === 'string') {
formattedDetails = details.replace(/\n/g, '<br>');
} else if (typeof details === 'object') {
formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '<br>');
} else {
formattedDetails = String(details);
}
}
content.innerHTML = `
<div class="mb-3">
<strong>Timestamp:</strong> ${new Date(log.timestamp).toLocaleString()}
</div>
<div class="mb-3">
<strong>Level:</strong> <span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span>
</div>
<div class="mb-3">
<strong>Category:</strong> ${log.category || '-'}
</div>
<div class="mb-3">
<strong>Action:</strong> ${log.action || '-'}
</div>
<div class="mb-3">
<strong>Description:</strong> ${log.description || '-'}
</div>
<div class="mb-3">
<strong>User:</strong> ${log.user || '-'}
</div>
<div class="mb-3">
<strong>IP Address:</strong> ${log.ip_address || '-'}
</div>
<div class="mb-3">
<strong>Details:</strong>
<div class="mt-2 p-3 bg-light rounded">
${formattedDetails}
</div>
</div>
`;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('logDetailsModal'));
modal.show();
})
.catch(error => {
console.error('Error viewing log details:', error);
showToast('Error loading log details', 'error');
});
}
// Helper function to get log level badge class
function getLogLevelBadgeClass(level) {
const classes = {
'INFO': 'info',
'WARNING': 'warning',
'ERROR': 'danger',
'DEBUG': 'secondary'
};
return classes[level] || 'secondary';
}
// Function to fetch logs
async function fetchLogs(page = 1) {
try {
const formData = new FormData(document.getElementById('logFiltersForm'));
const params = new URLSearchParams();
// Add form data to params
for (let [key, value] of formData.entries()) {
if (value) params.append(key, value);
}
// Add page number
params.append('page', page);
const response = await fetch(`/api/logs?${params.toString()}`);
if (!response.ok) throw new Error('Failed to fetch logs');
const data = await response.json();
const logsTableBody = document.getElementById('logsTableBody');
logsTableBody.innerHTML = '';
data.logs.forEach(log => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td><span class="badge bg-${getLogLevelBadgeClass(log.level)}">${log.level}</span></td>
<td>${log.category || '-'}</td>
<td>${log.action || '-'}</td>
<td>${log.description || '-'}</td>
<td>${log.user || '-'}</td>
<td>${log.ip_address || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewLogDetails(${log.id})">
<i class="fas fa-eye"></i>
</button>
</td>
`;
logsTableBody.appendChild(row);
});
// Update pagination
updateLogsPagination(data.current_page, data.total_pages);
} catch (error) {
console.error('Error fetching logs:', error);
showToast('Error loading logs', 'error');
}
}
// Function to update logs pagination
function updateLogsPagination(currentPage, totalPages) {
const pagination = document.getElementById('logsPagination');
if (!pagination) return;
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `
<button class="page-link" onclick="fetchLogs(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `
<button class="page-link" onclick="fetchLogs(${i})">${i}</button>
`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `
<button class="page-link" onclick="fetchLogs(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
pagination.appendChild(nextLi);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Initial load
fetchLogs();
// Form submit handler
document.getElementById('logFiltersForm').addEventListener('submit', function(e) {
e.preventDefault();
fetchLogs(1);
});
// Form reset handler
document.getElementById('logFiltersForm').addEventListener('reset', function(e) {
e.preventDefault();
this.reset();
fetchLogs(1);
});
});

View File

@@ -0,0 +1,51 @@
// Initialize all popovers
document.addEventListener('DOMContentLoaded', function() {
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function(popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl, {
html: true,
trigger: 'hover',
placement: 'right'
});
});
});
function testSmtpConnection() {
const form = document.getElementById('smtpSettingsForm');
const resultDiv = document.getElementById('testConnectionResult');
const button = event.target;
const originalText = button.innerHTML;
// Disable button and show loading state
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Testing...';
// Get form data
const formData = new FormData(form);
// Send test request
fetch('/settings/test-smtp', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('input[name="csrf_token"]').value,
'Content-Type': 'application/json',
},
body: JSON.stringify(Object.fromEntries(formData))
})
.then(response => response.json())
.then(data => {
if (data.success) {
resultDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>Connection successful!</div>';
} else {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Connection failed: ${data.error}</div>`;
}
})
.catch(error => {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Error: ${error.message}</div>`;
})
.finally(() => {
// Restore button state
button.disabled = false;
button.innerHTML = originalText;
});
}