From 082924a3ba46913f262b381d9c1f5602ebc80a18 Mon Sep 17 00:00:00 2001 From: Kobe Date: Wed, 28 May 2025 14:06:36 +0200 Subject: [PATCH] fixed messaging! --- __pycache__/extensions.cpython-313.pyc | Bin 493 -> 386 bytes __pycache__/models.cpython-313.pyc | Bin 17774 -> 17774 bytes app.py | 5 +- extensions.py | 4 +- requirements.txt | 1 - routes/__pycache__/__init__.cpython-313.pyc | Bin 2057 -> 2057 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 22200 -> 20141 bytes routes/conversations.py | 159 +++++--------- static/js/chat-manager.js | 203 +++++++++--------- static/js/conversation.js | 117 +++++----- templates/settings/tabs/logs.html | 99 +++++++++ 11 files changed, 315 insertions(+), 273 deletions(-) create mode 100644 templates/settings/tabs/logs.html diff --git a/__pycache__/extensions.cpython-313.pyc b/__pycache__/extensions.cpython-313.pyc index 587c43f7774152c17b82003b56212418efdc6030..e639f7094fa6f31a23332d92f8fa2184c0ee57fb 100644 GIT binary patch delta 228 zcmaFM+{CQ@nU|M~0SJ!&Hp{pSq#uJgFu(+5d=8qZu4x>@sKj6j6)9qiVMgMy$FL&t zIMP`(Szm%wPF$`V<)_JcOCT*Lu{b-vxG*O%CpjZEx3Y*CD0_<=BAk<-o|#v~0utea zh?JM4=_MBzr4_M)1esEjZt;Q@#pfpGC8nnq-C_aBPR?L-6ygV}UUZ4N~m`ODy delta 328 zcmZo-e#@->nU|M~0SMd8h+TmphFOWh z6e?T95yOhaC$Ap#SxsJoSj-q+7gTMe(_b6aRB_-Qs{5 yKbecsQB(w|2pAj;#YRBl12ZEd<6Q>j$+3)bf(Hq)$ delta 22 ccmaFY#rUp^k^3_*FBbz4?Cdn%$erv809j!M@Bjb+ diff --git a/app.py b/app.py index c4b416f..c9e7db3 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ from routes.trash import trash_bp from tasks import cleanup_trash import click from utils import timeago -from extensions import db, login_manager, csrf, socketio +from extensions import db, login_manager, csrf # Load environment variables load_dotenv() @@ -31,7 +31,6 @@ def create_app(): login_manager.init_app(app) login_manager.login_view = 'auth.login' csrf.init_app(app) - socketio.init_app(app) @app.context_processor def inject_csrf_token(): @@ -89,4 +88,4 @@ def profile_pic(filename): return send_from_directory(os.path.join(os.getcwd(), 'uploads', 'profile_pics'), filename) if __name__ == '__main__': - socketio.run(app, debug=True) \ No newline at end of file + app.run(debug=True) \ No newline at end of file diff --git a/extensions.py b/extensions.py index 6396315..eb3f77d 100644 --- a/extensions.py +++ b/extensions.py @@ -1,4 +1,3 @@ -from flask_socketio import SocketIO from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect @@ -6,5 +5,4 @@ from flask_wtf.csrf import CSRFProtect # Initialize extensions db = SQLAlchemy() login_manager = LoginManager() -csrf = CSRFProtect() -socketio = SocketIO(cors_allowed_origins="*") \ No newline at end of file +csrf = CSRFProtect() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 27949a6..e5572b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ WTForms==3.1.1 python-dotenv==1.0.1 psycopg2-binary==2.9.9 gunicorn==21.2.0 -Flask-SocketIO==5.3.6 email_validator==2.1.0.post1 \ No newline at end of file diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc index f929a5246799337db227e90f37a9ccfcaa44fff9..64feb4a8f08e60ce740d62c2852ab9156b00acd0 100644 GIT binary patch delta 20 acmeAa=oH}o%*)Hg00fM`%r$}!DFs&m diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index da68000b71aca98798aeae33ab3472fc0f596b89..28baa3885fae1b3f57813025675f2a545410f565 100644 GIT binary patch delta 5418 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 diff --git a/routes/conversations.py b/routes/conversations.py index 56c389c..3bcdbe9 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -1,13 +1,11 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user -from flask_socketio import emit, join_room, leave_room from models import db, Conversation, User, Message, MessageAttachment from forms import ConversationForm from routes.auth import require_password_change import os from werkzeug.utils import secure_filename from datetime import datetime -from extensions import socketio conversations_bp = Blueprint('conversations', __name__, url_prefix='/conversations') @@ -239,30 +237,45 @@ def delete_conversation(conversation_id): flash('Conversation has been deleted successfully.', 'success') return redirect(url_for('conversations.conversations')) -@socketio.on('join_conversation') +@conversations_bp.route('//messages', methods=['GET']) @login_required -def on_join(data): - conversation_id = data.get('conversation_id') +@require_password_change +def get_messages(conversation_id): conversation = Conversation.query.get_or_404(conversation_id) # Check if user is a member if not current_user.is_admin and current_user not in conversation.members: - return + return jsonify({'error': 'Unauthorized'}), 403 - # Join the room - join_room(f'conversation_{conversation_id}') - -@socketio.on('leave_conversation') -@login_required -def on_leave(data): - conversation_id = data.get('conversation_id') - leave_room(f'conversation_{conversation_id}') - -@socketio.on('heartbeat') -@login_required -def on_heartbeat(data): - # Just acknowledge the heartbeat to keep the connection alive - return {'status': 'ok'} + # Get the last message ID from the request + last_message_id = request.args.get('last_message_id', type=int) + + # Query for new messages + query = Message.query.filter_by(conversation_id=conversation_id) + if last_message_id: + query = query.filter(Message.id > last_message_id) + + messages = query.order_by(Message.created_at.asc()).all() + + # Format messages for response + message_data = [{ + 'id': message.id, + 'content': message.content, + 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), + 'sender_id': str(message.user_id), + 'sender_name': f"{message.user.username} {message.user.last_name}", + 'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'), + 'attachments': [{ + 'name': attachment.name, + 'size': attachment.size, + 'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index) + } for index, attachment in enumerate(message.attachments)] + } for message in messages] + + return jsonify({ + 'success': True, + 'messages': message_data + }) @conversations_bp.route('//send_message', methods=['POST']) @login_required @@ -272,59 +285,46 @@ def send_message(conversation_id): # Check if user is a member if not current_user.is_admin and current_user not in conversation.members: - return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403 + return jsonify({'error': 'Unauthorized'}), 403 message_content = request.form.get('message', '').strip() file_count = int(request.form.get('file_count', 0)) if not message_content and file_count == 0: - return jsonify({'success': False, 'error': 'Message or file is required.'}), 400 + return jsonify({'error': 'Message cannot be empty'}), 400 - # Create new message + # Create the message message = Message( content=message_content, - conversation_id=conversation_id, - user_id=current_user.id + user_id=current_user.id, + conversation_id=conversation_id ) - - # Create conversation-specific directory - conversation_dir = os.path.join(UPLOAD_FOLDER, str(conversation_id)) - os.makedirs(conversation_dir, exist_ok=True) + db.session.add(message) + db.session.flush() # Get the message ID # Handle file attachments attachments = [] for i in range(file_count): - file = request.files.get(f'file_{i}') - if file and file.filename: - if not allowed_file(file.filename): - return jsonify({'success': False, 'error': f'File type not allowed: {file.filename}'}), 400 - - if file.content_length and file.content_length > MAX_FILE_SIZE: - return jsonify({'success': False, 'error': f'File size exceeds limit: {file.filename}'}), 400 - - # Generate unique filename - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - filename = secure_filename(file.filename) - unique_filename = f"{timestamp}_{filename}" - file_path = os.path.join(conversation_dir, unique_filename) - - # Save file - file.save(file_path) - - # Create attachment record - attachment = MessageAttachment( - name=filename, - path=file_path, - type=get_file_extension(filename), - size=os.path.getsize(file_path) - ) - message.attachments.append(attachment) - attachments.append(attachment) + file_key = f'file_{i}' + if file_key in request.files: + file = request.files[file_key] + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(file_path) + + attachment = MessageAttachment( + message_id=message.id, + name=filename, + path=file_path, + size=os.path.getsize(file_path) + ) + db.session.add(attachment) + attachments.append(attachment) - db.session.add(message) db.session.commit() - # Prepare message data for WebSocket + # Prepare message data for response message_data = { 'id': message.id, 'content': message.content, @@ -339,10 +339,6 @@ def send_message(conversation_id): } for index, attachment in enumerate(attachments)] } - # Emit the message to all users in the conversation room - socketio.emit('new_message', message_data, room=f'conversation_{conversation_id}') - - # Return response with message data return jsonify({ 'success': True, 'message': message_data @@ -369,43 +365,4 @@ def download_attachment(message_id, attachment_index): ) except (IndexError, Exception) as e: flash('File not found.', 'error') - return redirect(url_for('conversations.conversation', conversation_id=conversation.id)) - -@conversations_bp.route('//messages') -@login_required -@require_password_change -def get_messages(conversation_id): - conversation = Conversation.query.get_or_404(conversation_id) - - # Check if user is a member - if not current_user.is_admin and current_user not in conversation.members: - return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403 - - # Get the last message ID from the request - last_message_id = request.args.get('last_message_id', type=int) - - # Query for new messages - query = Message.query.filter_by(conversation_id=conversation_id) - if last_message_id: - query = query.filter(Message.id > last_message_id) - - messages = query.order_by(Message.created_at.asc()).all() - - # Format messages for response - formatted_messages = [] - for message in messages: - formatted_messages.append({ - 'id': message.id, - 'content': message.content, - 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), - 'sender_id': str(message.user.id), - 'sender_name': f"{message.user.username} {message.user.last_name}", - 'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'), - 'attachments': [{ - 'name': attachment.name, - 'size': attachment.size, - 'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index) - } for index, attachment in enumerate(message.attachments)] - }) - - return jsonify({'success': True, 'messages': formatted_messages}) \ No newline at end of file + return redirect(url_for('conversations.conversation', conversation_id=conversation.id)) \ No newline at end of file diff --git a/static/js/chat-manager.js b/static/js/chat-manager.js index 652b268..e0c5476 100644 --- a/static/js/chat-manager.js +++ b/static/js/chat-manager.js @@ -1,127 +1,45 @@ -// Global state and socket management +// Global state and polling management if (typeof window.ChatManager === 'undefined') { window.ChatManager = (function() { let instance = null; - let socket = null; + let pollInterval = null; const state = { addedMessageIds: new Set(), messageQueue: new Set(), connectionState: { hasJoined: false, - isConnected: false, + isConnected: true, lastMessageId: null, - connectionAttempts: 0, - socketId: null + pollAttempts: 0 } }; function init(conversationId) { if (instance) { + console.log('[ChatManager] Instance already exists, returning existing instance'); return instance; } + console.log('[ChatManager] Initializing new instance for conversation:', conversationId); + // Initialize message IDs from existing messages $('.message').each(function() { const messageId = $(this).data('message-id'); if (messageId) { state.addedMessageIds.add(messageId); - } - }); - - // Create socket instance - socket = io(window.location.origin, { - path: '/socket.io/', - transports: ['polling', 'websocket'], - upgrade: true, - reconnection: true, - reconnectionAttempts: Infinity, - reconnectionDelay: 1000, - reconnectionDelayMax: 5000, - timeout: 20000, - autoConnect: true, - forceNew: true, - multiplex: false, - pingTimeout: 60000, - pingInterval: 25000, - upgradeTimeout: 10000, - rememberUpgrade: true, - rejectUnauthorized: false, - extraHeaders: { - 'X-Forwarded-Proto': 'https' - } - }); - - // Set up socket event handlers - socket.on('connect', function() { - state.connectionState.isConnected = true; - state.connectionState.connectionAttempts++; - state.connectionState.socketId = socket.id; - console.log('Socket connected:', { - attempt: state.connectionState.connectionAttempts, - socketId: socket.id, - existingSocketId: state.connectionState.socketId, - transport: socket.io.engine.transport.name - }); - - // Always rejoin the room on connect - console.log('Joining conversation room:', conversationId); - socket.emit('join_conversation', { - conversation_id: conversationId, - timestamp: new Date().toISOString(), - socketId: socket.id - }); - state.connectionState.hasJoined = true; - }); - - socket.on('disconnect', function(reason) { - console.log('Disconnected from conversation:', { - reason: reason, - socketId: socket.id, - connectionState: state.connectionState, - transport: socket.io.engine?.transport?.name - }); - state.connectionState.isConnected = false; - state.connectionState.socketId = null; - }); - - socket.on('connect_error', function(error) { - console.error('Connection error:', error); - // Try to reconnect with polling if websocket fails - if (socket.io.engine?.transport?.name === 'websocket') { - console.log('WebSocket failed, falling back to polling'); - socket.io.opts.transports = ['polling']; - } - }); - - socket.on('error', function(error) { - console.error('Socket error:', error); - }); - - // Add heartbeat to keep connection alive - setInterval(function() { - if (socket.connected) { - socket.emit('heartbeat', { - timestamp: new Date().toISOString(), - socketId: socket.id, - transport: socket.io.engine.transport.name + state.connectionState.lastMessageId = Math.max(state.connectionState.lastMessageId || 0, messageId); + console.log('[ChatManager] Initialized with existing message:', { + messageId: messageId, + lastMessageId: state.connectionState.lastMessageId }); } - }, 15000); - - // Handle transport upgrade - socket.io.engine.on('upgrade', function() { - console.log('Transport upgraded to:', socket.io.engine.transport.name); }); - socket.io.engine.on('upgradeError', function(err) { - console.error('Transport upgrade error:', err); - // Fall back to polling - socket.io.opts.transports = ['polling']; - }); + // Start polling for new messages + startPolling(conversationId); instance = { - socket: socket, state: state, cleanup: cleanup }; @@ -129,12 +47,97 @@ if (typeof window.ChatManager === 'undefined') { return instance; } + function startPolling(conversationId) { + console.log('[ChatManager] Starting polling for conversation:', conversationId); + + // Clear any existing polling + if (pollInterval) { + console.log('[ChatManager] Clearing existing polling interval'); + clearInterval(pollInterval); + } + + // Poll every 3 seconds + pollInterval = setInterval(() => { + console.log('[ChatManager] Polling interval triggered'); + fetchNewMessages(conversationId); + }, 3000); + + // Initial fetch + console.log('[ChatManager] Performing initial message fetch'); + fetchNewMessages(conversationId); + } + + function fetchNewMessages(conversationId) { + const url = `/conversations/${conversationId}/messages`; + const params = new URLSearchParams(); + + if (state.connectionState.lastMessageId) { + params.append('last_message_id', state.connectionState.lastMessageId); + } + + console.log('[ChatManager] Fetching new messages:', { + url: url, + params: params.toString(), + lastMessageId: state.connectionState.lastMessageId + }); + + fetch(`${url}?${params.toString()}`) + .then(response => response.json()) + .then(data => { + console.log('[ChatManager] Received messages response:', { + success: data.success, + messageCount: data.messages ? data.messages.length : 0, + messages: data.messages + }); + + if (data.success && data.messages) { + state.connectionState.pollAttempts = 0; + processNewMessages(data.messages); + } + }) + .catch(error => { + console.error('[ChatManager] Error fetching messages:', error); + state.connectionState.pollAttempts++; + + // If we've had too many failed attempts, try to reconnect + if (state.connectionState.pollAttempts > 5) { + console.log('[ChatManager] Too many failed attempts, restarting polling'); + startPolling(conversationId); + } + }); + } + + function processNewMessages(messages) { + console.log('[ChatManager] Processing new messages:', { + messageCount: messages.length, + currentLastMessageId: state.connectionState.lastMessageId, + existingMessageIds: Array.from(state.addedMessageIds) + }); + + messages.forEach(message => { + console.log('[ChatManager] Processing message:', { + messageId: message.id, + alreadyAdded: state.addedMessageIds.has(message.id), + currentLastMessageId: state.connectionState.lastMessageId + }); + + if (!state.addedMessageIds.has(message.id)) { + state.addedMessageIds.add(message.id); + state.connectionState.lastMessageId = Math.max(state.connectionState.lastMessageId || 0, message.id); + console.log('[ChatManager] Triggering new_message event for message:', message.id); + // Trigger the new message event + $(document).trigger('new_message', [message]); + } else { + console.log('[ChatManager] Skipping already added message:', message.id); + } + }); + } + function cleanup() { - console.log('Cleaning up socket connection'); - if (socket) { - socket.off('new_message'); - socket.disconnect(); - socket = null; + console.log('[ChatManager] Cleaning up polling'); + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; } instance = null; } diff --git a/static/js/conversation.js b/static/js/conversation.js index 36d2162..df55459 100644 --- a/static/js/conversation.js +++ b/static/js/conversation.js @@ -3,26 +3,28 @@ $(document).ready(function() { const conversationId = window.conversationId; // Set this in the template const currentUserId = window.currentUserId; // Set this in the template const chat = ChatManager.getInstance(conversationId); - const socket = chat.socket; const state = chat.state; - console.log('Initializing chat for conversation:', conversationId); + console.log('[Conversation] Initializing chat for conversation:', conversationId); - // Join conversation room - socket.on('connect', function() { - if (!state.connectionState.hasJoined) { - console.log('Joining conversation room:', conversationId); - socket.emit('join_conversation', { - conversation_id: conversationId, - timestamp: new Date().toISOString(), - socketId: socket.id - }); - state.connectionState.hasJoined = true; - } - }); + // Keep track of messages we've already displayed + const displayedMessageIds = new Set(); // Function to append a new message to the chat function appendMessage(message) { + console.log('[Conversation] Attempting to append message:', { + messageId: message.id, + content: message.content, + senderId: message.sender_id, + currentUserId: currentUserId + }); + + // Check if we've already displayed this message + if (displayedMessageIds.has(message.id)) { + return; + } + displayedMessageIds.add(message.id); + const isCurrentUser = message.sender_id === currentUserId; const messageHtml = `
@@ -65,9 +67,18 @@ $(document).ready(function() { $('.text-center.text-muted').remove(); $('#chatMessages').append(messageHtml); + console.log('[Conversation] Message appended to chat:', message.id); scrollToBottom(); } + // Initialize displayedMessageIds with existing messages + $('.message').each(function() { + const messageId = $(this).data('message-id'); + if (messageId) { + displayedMessageIds.add(messageId); + } + }); + // Scroll to bottom of chat messages function scrollToBottom() { const chatMessages = document.getElementById('chatMessages'); @@ -75,41 +86,14 @@ $(document).ready(function() { } scrollToBottom(); - // Message handling with deduplication and reconnection handling - socket.on('new_message', function(message) { - const timestamp = new Date().toISOString(); - const messageKey = `${message.id}-${socket.id}`; - - console.log('Message received:', { - id: message.id, - timestamp: timestamp, - socketId: socket.id, - messageKey: messageKey, - queueSize: state.messageQueue.size + // Listen for new messages + $(document).on('new_message', function(event, message) { + console.log('[Conversation] Received new_message event:', { + messageId: message.id, + eventType: event.type, + timestamp: new Date().toISOString() }); - - if (state.messageQueue.has(messageKey)) { - console.log('Message already in queue:', messageKey); - return; - } - - state.messageQueue.add(messageKey); - - if (!state.addedMessageIds.has(message.id)) { - console.log('Processing new message:', message.id); - appendMessage(message); - state.connectionState.lastMessageId = message.id; - state.addedMessageIds.add(message.id); - } else { - console.log('Duplicate message detected:', { - messageId: message.id, - lastMessageId: state.connectionState.lastMessageId, - socketId: socket.id - }); - } - - // Clean up message from queue after processing - state.messageQueue.delete(messageKey); + appendMessage(message); }); // Handle file selection @@ -123,14 +107,14 @@ $(document).ready(function() { } }); - // Handle message form submission with better error handling + // Handle message form submission let isSubmitting = false; $('#messageForm').off('submit').on('submit', function(e) { e.preventDefault(); e.stopPropagation(); if (isSubmitting) { - console.log('Message submission already in progress'); + console.log('[Conversation] Message submission already in progress'); return false; } @@ -143,15 +127,14 @@ $(document).ready(function() { const files = Array.from(fileInput.files); if (!message && files.length === 0) { - console.log('Empty message submission attempted'); + console.log('[Conversation] Empty message submission attempted'); return false; } - console.log('Submitting message:', { + console.log('[Conversation] Submitting message:', { hasText: !!message, fileCount: files.length, - socketId: socket.id, - connectionState: state.connectionState + timestamp: new Date().toISOString() }); isSubmitting = true; @@ -163,44 +146,46 @@ $(document).ready(function() { const formData = new FormData(); formData.append('message', message); formData.append('csrf_token', $('input[name="csrf_token"]').val()); - formData.append('socket_id', socket.id); + formData.append('file_count', files.length); files.forEach((file, index) => { formData.append(`file_${index}`, file); }); - formData.append('file_count', files.length); $.ajax({ - url: window.sendMessageUrl, // Set this in the template + url: window.sendMessageUrl, method: 'POST', data: formData, processData: false, contentType: false, success: function(response) { - console.log('Message sent successfully:', { + console.log('[Conversation] Message sent successfully:', { response: response, - socketId: socket.id + timestamp: new Date().toISOString() }); if (response.success) { messageInput.val(''); fileInput.value = ''; $('#selectedFiles').text(''); - // If socket is disconnected, append message directly - if (!state.connectionState.isConnected && response.message) { - console.log('Socket disconnected, appending message directly'); + // Append the message directly since we sent it + if (response.message) { + console.log('[Conversation] Appending sent message directly:', response.message.id); + // Update the ChatManager's lastMessageId + chat.state.connectionState.lastMessageId = response.message.id; appendMessage(response.message); } } else { - console.error('Message send failed:', response); + console.error('[Conversation] Message send failed:', response); alert('Failed to send message. Please try again.'); } }, error: function(xhr, status, error) { - console.error('Failed to send message:', { + console.error('[Conversation] Failed to send message:', { status: status, error: error, - response: xhr.responseText + response: xhr.responseText, + timestamp: new Date().toISOString() }); alert('Failed to send message. Please try again.'); }, @@ -210,6 +195,7 @@ $(document).ready(function() { submitIcon.removeClass('d-none'); spinner.addClass('d-none'); isSubmitting = false; + console.log('[Conversation] Message submission completed'); } }); @@ -218,6 +204,7 @@ $(document).ready(function() { // Clean up on page unload $(window).on('beforeunload', function() { + console.log('[Conversation] Cleaning up on page unload'); chat.cleanup(); }); }); \ No newline at end of file diff --git a/templates/settings/tabs/logs.html b/templates/settings/tabs/logs.html new file mode 100644 index 0000000..c21f6b3 --- /dev/null +++ b/templates/settings/tabs/logs.html @@ -0,0 +1,99 @@ +{% macro logs_tab() %} +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + +
TimestampLevelCategoryActionDescriptionUserIP Address
+
+ + + +
+
+
+
+ + + +{% endmacro %} \ No newline at end of file