diff --git a/static/js/instance_detail.js b/static/js/instance_detail.js
new file mode 100644
index 0000000..fe62eaf
--- /dev/null
+++ b/static/js/instance_detail.js
@@ -0,0 +1,1138 @@
+// Basic test to verify script execution
+console.log('Script block loaded');
+
+// Test function to verify DOM elements exist
+function testElements() {
+ console.log('Testing DOM elements...');
+ const elements = [
+ 'company-name-value',
+ 'company-industry',
+ 'company-description',
+ 'company-email',
+ 'company-phone',
+ 'company-website',
+ 'company-address'
+ ];
+
+ elements.forEach(id => {
+ const element = document.getElementById(id);
+ console.log(`Element ${id} exists:`, !!element);
+ });
+}
+
+let statusUpdateInterval;
+
+// Function to check instance status
+async function checkInstanceStatus() {
+ console.log('checkInstanceStatus called');
+ try {
+ const response = await fetch(`/instances/{{ instance.id }}/status`);
+ if (!response.ok) throw new Error('Failed to check instance status');
+
+ const data = await response.json();
+ console.log('Status data:', data);
+
+ // Update activity status
+ const activityIcon = document.querySelector('.card:first-child .fa-circle-notch');
+ const activityBadge = document.querySelector('.card:first-child .badge');
+ const activityDetails = document.querySelector('.card:first-child .text-muted');
+
+ if (activityIcon) {
+ activityIcon.className = `fas fa-circle-notch fa-spin ${data.status === 'active' ? 'text-success' : 'text-danger'} fa-2x`;
+ }
+ if (activityBadge) {
+ activityBadge.className = `badge bg-${data.status === 'active' ? 'success' : 'danger'}`;
+ activityBadge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
+ }
+ if (activityDetails) {
+ // Only update if we have status details
+ if (data.status_details) {
+ activityDetails.textContent = data.status_details;
+ activityDetails.style.display = 'block';
+ } else {
+ activityDetails.style.display = 'none';
+ }
+ }
+ } catch (error) {
+ console.error('Error checking instance status:', error);
+ }
+}
+
+// Function to check authentication status
+async function checkAuthStatus() {
+ console.log('checkAuthStatus called');
+ try {
+ const response = await fetch(`/instances/{{ instance.id }}/auth-status`);
+ if (!response.ok) throw new Error('Failed to check authentication status');
+
+ const data = await response.json();
+ console.log('Auth data:', data);
+
+ // Update authentication status
+ const authIcon = document.querySelector('.card:nth-child(2) .fa-shield-alt');
+ const authBadge = document.querySelector('.card:nth-child(2) .badge');
+ const authDetails = document.querySelector('.card:nth-child(2) .text-muted');
+
+ if (authIcon) {
+ authIcon.className = `fas fa-shield-alt ${data.authenticated ? 'text-success' : 'text-warning'} fa-2x`;
+ }
+ if (authBadge) {
+ authBadge.className = `badge bg-${data.authenticated ? 'success' : 'warning'}`;
+ authBadge.textContent = data.authenticated ? 'Authenticated' : 'Not Authenticated';
+ }
+ if (authDetails) {
+ authDetails.textContent = data.authenticated ? '' : 'This instance needs to be authenticated';
+ }
+ } catch (error) {
+ console.error('Error checking authentication status:', error);
+ }
+}
+
+// Function to fetch company information
+async function fetchCompanyInfo() {
+ console.log('fetchCompanyInfo called');
+ try {
+ // First get JWT token
+ console.log('Getting management token...');
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ console.log('Token response status:', tokenResponse.status);
+ if (!tokenResponse.ok) {
+ console.error('Failed to get management token:', tokenResponse.status);
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ console.log('Token data received:', tokenData);
+
+ if (!tokenData.token) {
+ console.error('No token in response');
+ throw new Error('No token received');
+ }
+
+ // Then fetch settings using the JWT token
+ console.log('Fetching settings from:', `{{ instance.main_url }}/api/admin/settings`);
+ const response = await fetch(`{{ instance.main_url }}/api/admin/settings`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ console.log('Settings response status:', response.status);
+ if (!response.ok) {
+ console.error('Settings fetch failed:', response.status);
+ throw new Error('Failed to fetch company information');
+ }
+
+ const data = await response.json();
+ console.log('Settings data received:', data);
+
+ // Update company name separately with debugging
+ const companyNameElement = document.getElementById('company-name-value');
+ const companyNameLabel = document.getElementById('company-name-label');
+
+ // Ensure label text is set and maintained
+ if (companyNameLabel) {
+ companyNameLabel.textContent = 'Company Name:';
+ }
+
+ if (companyNameElement) {
+ companyNameElement.textContent = data.company_name || 'Not set';
+ }
+
+ // Update other company details
+ const updateElement = (id, value) => {
+ const element = document.getElementById(id);
+ if (element && element.classList.contains('company-value')) {
+ element.textContent = value || 'Not set';
+ }
+ };
+
+ // Update other company details
+ updateElement('company-industry', data.company_industry);
+ updateElement('company-description', data.company_description);
+
+ // Update contact information
+ const emailElement = document.getElementById('company-email');
+ const phoneElement = document.getElementById('company-phone');
+ const websiteElement = document.getElementById('company-website');
+
+ // Update email with mailto link
+ if (emailElement && emailElement.classList.contains('company-value')) {
+ if (data.company_email && data.company_email !== 'Not set') {
+ emailElement.innerHTML = `
+ ${data.company_email}
+ `;
+ } else {
+ emailElement.textContent = 'Not set';
+ }
+ }
+
+ // Update phone with tel link
+ if (phoneElement && phoneElement.classList.contains('company-value')) {
+ if (data.company_phone && data.company_phone !== 'Not set') {
+ phoneElement.innerHTML = `
+ ${data.company_phone}
+ `;
+ } else {
+ phoneElement.textContent = 'Not set';
+ }
+ }
+
+ // Update website with external link
+ if (websiteElement && websiteElement.classList.contains('company-value')) {
+ if (data.company_website && data.company_website !== 'Not set') {
+ // Ensure website has http/https prefix
+ let websiteUrl = data.company_website;
+ if (!websiteUrl.startsWith('http://') && !websiteUrl.startsWith('https://')) {
+ websiteUrl = 'https://' + websiteUrl;
+ }
+ websiteElement.innerHTML = `
+ ${data.company_website}
+ `;
+ } else {
+ websiteElement.textContent = 'Not set';
+ }
+ }
+
+ // Format address
+ const addressParts = [
+ data.company_address,
+ data.company_city,
+ data.company_state,
+ data.company_zip,
+ data.company_country
+ ].filter(Boolean);
+ updateElement('company-address', addressParts.length ? addressParts.join(', ') : 'Not set');
+
+ console.log('Company info update complete');
+ } catch (error) {
+ console.error('Error in fetchCompanyInfo:', error);
+ // Show error state in all fields
+ const fields = ['company-name-value', 'company-industry', 'company-description',
+ 'company-email', 'company-phone', 'company-website', 'company-address'];
+ fields.forEach(field => {
+ const element = document.getElementById(field);
+ if (element && element.classList.contains('company-value')) {
+ element.textContent = 'Error loading data';
+ }
+ });
+ }
+}
+
+// Function to update all statuses
+async function updateAllStatuses() {
+ console.log('Starting updateAllStatuses...');
+ try {
+ await Promise.all([
+ checkInstanceStatus(),
+ checkAuthStatus(),
+ fetchCompanyInfo()
+ ]);
+ console.log('All statuses updated successfully');
+ } catch (error) {
+ console.error('Error in updateAllStatuses:', error);
+ }
+}
+
+// Initialize status updates
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('DOM loaded, initializing updates...');
+ testElements(); // Test if elements exist
+ // Initial update
+ updateAllStatuses();
+
+ // Set up periodic updates (every 10 minutes)
+ statusUpdateInterval = setInterval(updateAllStatuses, 600000);
+ console.log('Periodic updates initialized');
+});
+
+// Clean up interval when page is unloaded
+window.addEventListener('beforeunload', function() {
+ if (statusUpdateInterval) {
+ clearInterval(statusUpdateInterval);
+ }
+});
+
+// Contacts Management Functions
+async function fetchContacts() {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch contacts');
+ }
+
+ const contacts = await response.json();
+ const contactsList = document.getElementById('contactsTableBody');
+ contactsList.innerHTML = '';
+
+ contacts.forEach(contact => {
+ const row = document.createElement('tr');
+
+ // Role badge color mapping
+ const roleBadgeClass = {
+ 'admin': 'bg-danger',
+ 'manager': 'bg-warning',
+ 'user': 'bg-info'
+ }[contact.role] || 'bg-secondary';
+
+ // Status badge color mapping
+ const statusBadgeClass = contact.is_active ? 'bg-success' : 'bg-secondary';
+
+ // Format phone number for tel: link (remove any non-digit characters)
+ const phoneNumber = contact.phone ? contact.phone.replace(/\D/g, '') : '';
+
+ row.innerHTML = `
+
${contact.username} ${contact.last_name} |
+
+
+
+ ${contact.email}
+
+
+ |
+
+ ${contact.phone ? `
+
+
+ ${contact.phone}
+
+
+ ` : '-'}
+ |
+ ${contact.company || '-'} |
+ ${contact.position || '-'} |
+ ${contact.role.charAt(0).toUpperCase() + contact.role.slice(1)} |
+ ${contact.is_active ? 'Active' : 'Inactive'} |
+
+
+
+
+
+ |
+ `;
+ contactsList.appendChild(row);
+ });
+ } catch (error) {
+ console.error('Error fetching contacts:', error);
+ showToast('Error loading contacts', 'error');
+ }
+}
+
+async function addContact(event) {
+ event.preventDefault();
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const formData = new FormData(event.target);
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ },
+ body: JSON.stringify({
+ username: formData.get('name'),
+ email: formData.get('email'),
+ phone: formData.get('phone'),
+ company: formData.get('company'),
+ position: formData.get('position'),
+ role: formData.get('role'),
+ is_active: formData.get('status') === 'active'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to add contact');
+ }
+
+ // Close modal and refresh list
+ const modal = bootstrap.Modal.getInstance(document.getElementById('addContactModal'));
+ modal.hide();
+ fetchContacts();
+
+ // Show success message
+ showToast('Contact added successfully', 'success');
+ } catch (error) {
+ console.error('Error adding contact:', error);
+ showToast('Error adding contact', 'error');
+ }
+}
+
+async function editContact(id) {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch contact details');
+ }
+
+ const contact = await response.json();
+
+ // Populate form
+ document.getElementById('edit-contact-id').value = contact.id;
+ document.getElementById('edit-name').value = contact.username;
+ document.getElementById('edit-email').value = contact.email || '';
+ document.getElementById('edit-phone').value = contact.phone || '';
+ document.getElementById('edit-company').value = contact.company || '';
+ document.getElementById('edit-position').value = contact.position || '';
+ document.getElementById('edit-role').value = contact.role;
+ document.getElementById('edit-status').value = contact.is_active ? 'active' : 'inactive';
+
+ // Show modal
+ const modal = new bootstrap.Modal(document.getElementById('editContactModal'));
+ modal.show();
+ } catch (error) {
+ console.error('Error fetching contact details:', error);
+ showToast('Error loading contact details', 'error');
+ }
+}
+
+async function updateContact(event) {
+ event.preventDefault();
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const formData = new FormData(event.target);
+ const id = formData.get('id');
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
+ method: 'PUT',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ },
+ body: JSON.stringify({
+ username: formData.get('name'),
+ email: formData.get('email'),
+ phone: formData.get('phone'),
+ company: formData.get('company'),
+ position: formData.get('position'),
+ role: formData.get('role'),
+ is_active: formData.get('status') === 'active'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update contact');
+ }
+
+ // Close modal and refresh list
+ const modal = bootstrap.Modal.getInstance(document.getElementById('editContactModal'));
+ modal.hide();
+ fetchContacts();
+
+ // Show success message
+ showToast('Contact updated successfully', 'success');
+ } catch (error) {
+ console.error('Error updating contact:', error);
+ showToast('Error updating contact', 'error');
+ }
+}
+
+async function deleteContact(id) {
+ // Store the contact ID for the delete confirmation
+ const deleteModal = new bootstrap.Modal(document.getElementById('deleteContactModal'));
+ document.getElementById('confirmDelete').onclick = async () => {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete contact');
+ }
+
+ // Close modal and refresh list
+ deleteModal.hide();
+ fetchContacts();
+
+ // Show success message
+ showToast('Contact deleted successfully', 'success');
+ } catch (error) {
+ console.error('Error deleting contact:', error);
+ showToast('Error deleting contact', 'error');
+ }
+ };
+
+ // Show the delete confirmation modal
+ deleteModal.show();
+}
+
+// Helper function to show toast messages
+function showToast(title, message, type = 'info') {
+ const toast = document.createElement('div');
+ toast.className = `toast align-items-center text-white bg-${type} border-0`;
+ toast.setAttribute('role', 'alert');
+ toast.setAttribute('aria-live', 'assertive');
+ toast.setAttribute('aria-atomic', 'true');
+
+ toast.innerHTML = `
+
+
+ ${title}
+ ${message}
+
+
+
+ `;
+
+ const toastContainer = document.getElementById('toastContainer') || (() => {
+ const container = document.createElement('div');
+ container.id = 'toastContainer';
+ container.className = 'toast-container position-fixed bottom-0 end-0 p-3';
+ document.body.appendChild(container);
+ return container;
+ })();
+
+ toastContainer.appendChild(toast);
+ const bsToast = new bootstrap.Toast(toast);
+ bsToast.show();
+
+ toast.addEventListener('hidden.bs.toast', () => {
+ toast.remove();
+ });
+}
+
+// Event Listeners
+document.addEventListener('DOMContentLoaded', function() {
+ // ... existing event listeners ...
+
+ // Fetch contacts when contacts tab is shown
+ document.getElementById('contacts-tab').addEventListener('shown.bs.tab', fetchContacts);
+
+ // Fetch logs when logs tab is shown
+ document.getElementById('logs-tab').addEventListener('shown.bs.tab', fetchLogs);
+
+ // Fetch mails when mails tab is shown
+ document.getElementById('mails-tab').addEventListener('shown.bs.tab', fetchMails);
+
+ // Debug the company name label
+ const label = document.getElementById('company-name-label');
+ if (label) {
+ console.log('Company name label found:', label);
+
+ // Create a MutationObserver to watch for changes
+ const observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ console.log('Label changed:', mutation);
+ console.log('Old value:', mutation.oldValue);
+ console.log('New value:', label.textContent);
+ });
+ });
+
+ // Start observing
+ observer.observe(label, {
+ attributes: true,
+ childList: true,
+ characterData: true,
+ subtree: true
+ });
+ } else {
+ console.log('Company name label not found');
+ }
+});
+
+// Function to fetch logs
+async function fetchLogs() {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/logs`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch logs');
+ }
+
+ const data = await response.json();
+ const logsTableBody = document.getElementById('logsTableBody');
+ logsTableBody.innerHTML = '';
+
+ // Fetch all users to map IDs to names
+ const usersResponse = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!usersResponse.ok) {
+ throw new Error('Failed to fetch users');
+ }
+
+ const usersData = await usersResponse.json();
+ const userMap = new Map(usersData.map(user => [user.id, `${user.username} ${user.last_name}`]));
+
+ data.events.forEach(event => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${new Date(event.timestamp).toLocaleString()} |
+ ${event.event_type || '-'} |
+ ${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'} |
+ ${event.ip_address || '-'} |
+ `;
+ logsTableBody.appendChild(row);
+ });
+
+ // Update pagination
+ updateLogsPagination(data.current_page, data.pages);
+ } catch (error) {
+ console.error('Error fetching logs:', error);
+ showToast('Error loading logs', 'error');
+ }
+}
+
+// Function to fetch mails
+async function fetchMails() {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch mail logs');
+ }
+
+ const data = await response.json();
+ const mailsTableBody = document.querySelector('#mails .table tbody');
+ mailsTableBody.innerHTML = '';
+
+ data.mails.forEach(mail => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${new Date(mail.created_at).toLocaleString()} |
+ ${mail.recipient} |
+ ${mail.subject} |
+ ${mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-'} |
+ `;
+ mailsTableBody.appendChild(row);
+ });
+
+ // Update pagination
+ updateMailsPagination(data.current_page, data.pages);
+ } catch (error) {
+ console.error('Error fetching mail logs:', error);
+ showToast('Error loading mail logs', 'error');
+ }
+}
+
+// Helper function to get mail status badge class
+function getMailStatusBadgeClass(status) {
+ const classes = {
+ 'pending': 'warning',
+ 'sent': 'success',
+ 'failed': 'danger'
+ };
+ return classes[status] || 'secondary';
+}
+
+// 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 = `
+
+ `;
+ 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 = `
+
+ `;
+ pagination.appendChild(li);
+ }
+
+ // Next button
+ const nextLi = document.createElement('li');
+ nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
+ nextLi.innerHTML = `
+
+ `;
+ pagination.appendChild(nextLi);
+}
+
+// Function to update mails pagination
+function updateMailsPagination(currentPage, totalPages) {
+ const pagination = document.querySelector('#mails .pagination');
+ if (!pagination) return;
+
+ pagination.innerHTML = '';
+
+ // Previous button
+ const prevLi = document.createElement('li');
+ prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
+ prevLi.innerHTML = `
+
+ `;
+ 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 = `
+
+ `;
+ pagination.appendChild(li);
+ }
+
+ // Next button
+ const nextLi = document.createElement('li');
+ nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
+ nextLi.innerHTML = `
+
+ `;
+ pagination.appendChild(nextLi);
+}
+
+// Function to change logs page
+async function changeLogsPage(page) {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/logs?page=${page}`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch logs');
+ }
+
+ const data = await response.json();
+
+ // Fetch all users to map IDs to names
+ const usersResponse = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!usersResponse.ok) {
+ throw new Error('Failed to fetch users');
+ }
+
+ const usersData = await usersResponse.json();
+ const userMap = new Map(usersData.map(user => [user.id, `${user.username} ${user.last_name}`]));
+
+ const logsTableBody = document.getElementById('logsTableBody');
+ logsTableBody.innerHTML = '';
+
+ data.events.forEach(event => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${new Date(event.timestamp).toLocaleString()} |
+ ${event.event_type || '-'} |
+ ${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'} |
+ ${event.ip_address || '-'} |
+ `;
+ logsTableBody.appendChild(row);
+ });
+
+ // Update pagination
+ updateLogsPagination(data.current_page, data.pages);
+ } catch (error) {
+ console.error('Error changing logs page:', error);
+ showToast('Error loading logs', 'error');
+ }
+}
+
+// Function to change mails page
+async function changeMailsPage(page) {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs?page=${page}`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch mail logs');
+ }
+
+ const data = await response.json();
+ const mailsTableBody = document.querySelector('#mails .table tbody');
+ mailsTableBody.innerHTML = '';
+
+ data.mails.forEach(mail => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${new Date(mail.created_at).toLocaleString()} |
+ ${mail.recipient} |
+ ${mail.subject} |
+ ${mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-'} |
+ `;
+ mailsTableBody.appendChild(row);
+ });
+
+ // Update pagination
+ updateMailsPagination(data.current_page, data.pages);
+ } catch (error) {
+ console.error('Error changing mail logs page:', error);
+ showToast('Error loading mail logs', 'error');
+ }
+}
+
+// Function to view mail details
+async function viewMailDetails(mailId) {
+ try {
+ // First get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!tokenResponse.ok) {
+ throw new Error('Failed to get management token');
+ }
+
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) {
+ throw new Error('No token received');
+ }
+
+ const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs/${mailId}`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch mail details');
+ }
+
+ const mail = await response.json();
+
+ // Update modal content
+ document.getElementById('modalRecipient').textContent = mail.recipient;
+ document.getElementById('modalStatus').innerHTML = `
+ ${mail.status}
+ `;
+ document.getElementById('modalTemplate').textContent = mail.template_id ? mail.template_id : '-';
+ document.getElementById('modalCreatedAt').textContent = new Date(mail.created_at).toLocaleString();
+ document.getElementById('modalSentAt').textContent = mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-';
+ document.getElementById('modalSubject').textContent = mail.subject;
+ document.getElementById('modalBody').innerHTML = mail.body;
+
+ // Show modal
+ const modal = new bootstrap.Modal(document.getElementById('mailDetailsModal'));
+ modal.show();
+ } catch (error) {
+ console.error('Error viewing mail details:', error);
+ showToast('Error loading mail details', 'error');
+ }
+}
+
+// Function to view log details
+function viewLogDetails(event) {
+ const modal = new bootstrap.Modal(document.getElementById('logDetailsModal'));
+ const content = document.getElementById('logDetailsContent');
+
+ // Format the details in a readable way
+ let details = event.details;
+ let formattedDetails = 'No details available';
+
+ if (details) {
+ if (typeof details === 'string') {
+ formattedDetails = details.replace(/\n/g, '
');
+ } else if (typeof details === 'object') {
+ formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '
');
+ } else {
+ formattedDetails = String(details);
+ }
+ }
+
+ content.innerHTML = `
+
+ Timestamp: ${new Date(event.timestamp).toLocaleString()}
+
+
+ Event Type: ${event.event_type || '-'}
+
+
+ User: ${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'}
+
+
+ IP Address: ${event.ip_address || '-'}
+
+
+
Details:
+
+ ${formattedDetails}
+
+
+ `;
+
+ modal.show();
+}
+
+// Add event listener for clear filters button
+document.addEventListener('DOMContentLoaded', function() {
+ // ... existing event listeners ...
+
+ const clearLogFiltersBtn = document.getElementById('clearLogFiltersBtn');
+ if (clearLogFiltersBtn) {
+ clearLogFiltersBtn.addEventListener('click', function() {
+ document.getElementById('logCategory').value = '';
+ document.getElementById('dateRangeFilter').value = '7d';
+ fetchLogs();
+ });
+ }
+});
+
+// Function to fetch version and payment plan info
+async function fetchInstanceVersionAndPlan() {
+ const versionEl = document.getElementById('instance-version-value');
+ const planEl = document.getElementById('instance-payment-plan-value');
+ versionEl.textContent = 'Loading...';
+ planEl.textContent = 'Loading...';
+ try {
+ // Get JWT token
+ const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': '{{ instance.connection_token }}',
+ 'Accept': 'application/json'
+ }
+ });
+ if (!tokenResponse.ok) throw new Error('Failed to get management token');
+ const tokenData = await tokenResponse.json();
+ if (!tokenData.token) throw new Error('No token received');
+ // Fetch version info
+ const response = await fetch(`{{ instance.main_url }}/api/admin/version-info`, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${tokenData.token}`
+ }
+ });
+ if (!response.ok) throw new Error('Failed to fetch version info');
+ const data = await response.json();
+ versionEl.textContent = data.app_version || 'Unknown';
+ planEl.textContent = data.pricing_tier_name || 'Unknown';
+ } catch (error) {
+ versionEl.textContent = 'Error';
+ planEl.textContent = 'Error';
+ console.error('Error fetching version/plan info:', error);
+ }
+}
+document.addEventListener('DOMContentLoaded', function() {
+ // ... existing code ...
+ fetchInstanceVersionAndPlan();
+});
+
diff --git a/templates/main/instance_detail.html b/templates/main/instance_detail.html
index 2fe11d8..105897a 100644
--- a/templates/main/instance_detail.html
+++ b/templates/main/instance_detail.html
@@ -499,1143 +499,19 @@
{% block extra_js %}
-{% endblock %}
\ No newline at end of file
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/main/instances.html b/templates/main/instances.html
index b458f8f..c14fdf1 100644
--- a/templates/main/instances.html
+++ b/templates/main/instances.html
@@ -649,5 +649,5 @@
{% endblock %}
{% block extra_js %}
-
+
{% endblock %}
\ No newline at end of file