redesign of logger frontend to streamline and unify all logger views
This commit is contained in:
parent
8f064cda34
commit
4a10052eae
@ -2,7 +2,7 @@
|
||||
import csv
|
||||
import io
|
||||
import datetime
|
||||
from flask import Blueprint, render_template, request, send_file
|
||||
from flask import Blueprint, render_template, request, send_file, jsonify
|
||||
from ..db import db
|
||||
from ..models import ActivityLog, ActivityCategory
|
||||
|
||||
@ -11,10 +11,10 @@ bp = Blueprint("logger", __name__, url_prefix="/logs")
|
||||
|
||||
@bp.route("/")
|
||||
def list_logs():
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = 50
|
||||
|
||||
# Filters
|
||||
# For the new modern view, we only need to provide initial filter values and categories
|
||||
# The actual data loading will be handled by JavaScript via the API endpoint
|
||||
|
||||
# Get filter parameters for initial state
|
||||
category = request.args.get("category")
|
||||
start_date = request.args.get("start_date")
|
||||
end_date = request.args.get("end_date")
|
||||
@ -23,31 +23,10 @@ def list_logs():
|
||||
if search_term == "None":
|
||||
search_term = None
|
||||
|
||||
|
||||
query = ActivityLog.query
|
||||
|
||||
if category:
|
||||
query = query.filter(ActivityLog.category == category)
|
||||
if start_date:
|
||||
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(ActivityLog.timestamp >= start_date_dt)
|
||||
if end_date:
|
||||
end_date_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
|
||||
query = query.filter(ActivityLog.timestamp <= end_date_dt)
|
||||
if search_term:
|
||||
query = query.filter(db.or_(
|
||||
ActivityLog.action.contains(search_term),
|
||||
ActivityLog.description.contains(search_term)
|
||||
))
|
||||
|
||||
pagination = query.order_by(ActivityLog.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
categories = [e.value for e in ActivityCategory]
|
||||
|
||||
return render_template(
|
||||
"logger.html.jinja",
|
||||
logs=pagination.items,
|
||||
pagination=pagination,
|
||||
"logs.html.jinja",
|
||||
categories=categories,
|
||||
category=category,
|
||||
start_date=start_date,
|
||||
@ -99,8 +78,12 @@ def download_logs():
|
||||
|
||||
# Create response
|
||||
filename = f"logs_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
csv_data.seek(0)
|
||||
output = io.BytesIO(csv_data.getvalue().encode('utf-8'))
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
io.StringIO(csv_data.getvalue()),
|
||||
output,
|
||||
mimetype="text/csv",
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
@ -109,4 +92,132 @@ def download_logs():
|
||||
@bp.route("/<int:log_id>/detail")
|
||||
def log_detail(log_id):
|
||||
log = ActivityLog.query.get_or_404(log_id)
|
||||
return render_template("partials/log_detail_modal.html.jinja", log=log)
|
||||
return render_template("partials/log_detail_modal.html.jinja", log=log)
|
||||
|
||||
|
||||
@bp.route("/api")
|
||||
def get_logs_api():
|
||||
"""Unified API endpoint for getting activity logs with filtering and pagination support."""
|
||||
try:
|
||||
# Pagination parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 50, type=int)
|
||||
|
||||
# Legacy limit parameter for backward compatibility
|
||||
limit = request.args.get('limit', type=int)
|
||||
if limit and not request.args.get('page'):
|
||||
# Legacy mode: use limit without pagination
|
||||
query = ActivityLog.query
|
||||
|
||||
# Apply filters
|
||||
categories = request.args.getlist('category')
|
||||
if categories:
|
||||
query = query.filter(ActivityLog.category.in_(categories))
|
||||
|
||||
status = request.args.get('status')
|
||||
if status:
|
||||
query = query.filter(ActivityLog.status == status)
|
||||
|
||||
start_date = request.args.get('start_date')
|
||||
if start_date:
|
||||
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(ActivityLog.timestamp >= start_date_dt)
|
||||
|
||||
end_date = request.args.get('end_date')
|
||||
if end_date:
|
||||
end_date_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
|
||||
query = query.filter(ActivityLog.timestamp <= end_date_dt)
|
||||
|
||||
search_term = request.args.get('search_term')
|
||||
if search_term and search_term != "None":
|
||||
query = query.filter(db.or_(
|
||||
ActivityLog.action.contains(search_term),
|
||||
ActivityLog.description.contains(search_term)
|
||||
))
|
||||
|
||||
logs = query.order_by(ActivityLog.timestamp.desc()).limit(limit).all()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"logs": [{
|
||||
"id": log.id,
|
||||
"timestamp": log.timestamp.isoformat(),
|
||||
"action": log.action,
|
||||
"status": log.status,
|
||||
"description": log.description,
|
||||
"category": log.category,
|
||||
"paper_id": log.paper_id,
|
||||
"extra_data": log.extra_data
|
||||
} for log in logs]
|
||||
})
|
||||
|
||||
# Ensure reasonable per_page limits
|
||||
per_page = min(per_page, 100) # Cap at 100 items per page
|
||||
|
||||
# Build query with filtering
|
||||
query = ActivityLog.query
|
||||
|
||||
# Filter by categories if specified
|
||||
categories = request.args.getlist('category')
|
||||
if categories:
|
||||
query = query.filter(ActivityLog.category.in_(categories))
|
||||
|
||||
# Filter by status if specified
|
||||
status = request.args.get('status')
|
||||
if status:
|
||||
query = query.filter(ActivityLog.status == status)
|
||||
|
||||
# Date filters
|
||||
start_date = request.args.get('start_date')
|
||||
if start_date:
|
||||
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(ActivityLog.timestamp >= start_date_dt)
|
||||
|
||||
end_date = request.args.get('end_date')
|
||||
if end_date:
|
||||
end_date_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
|
||||
query = query.filter(ActivityLog.timestamp <= end_date_dt)
|
||||
|
||||
# Search term filter
|
||||
search_term = request.args.get('search_term')
|
||||
if search_term and search_term != "None":
|
||||
query = query.filter(db.or_(
|
||||
ActivityLog.action.contains(search_term),
|
||||
ActivityLog.description.contains(search_term)
|
||||
))
|
||||
|
||||
# Order by most recent first and paginate
|
||||
pagination = query.order_by(ActivityLog.timestamp.desc()).paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
error_out=False
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"logs": [{
|
||||
"id": log.id,
|
||||
"timestamp": log.timestamp.isoformat(),
|
||||
"action": log.action,
|
||||
"status": log.status,
|
||||
"description": log.description,
|
||||
"category": log.category,
|
||||
"paper_id": log.paper_id,
|
||||
"extra_data": log.extra_data
|
||||
} for log in pagination.items],
|
||||
"pagination": {
|
||||
"page": pagination.page,
|
||||
"pages": pagination.pages,
|
||||
"per_page": pagination.per_page,
|
||||
"total": pagination.total,
|
||||
"has_next": pagination.has_next,
|
||||
"has_prev": pagination.has_prev,
|
||||
"next_num": pagination.next_num if pagination.has_next else None,
|
||||
"prev_num": pagination.prev_num if pagination.has_prev else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Error getting logs: {str(e)}"
|
||||
}), 500
|
@ -255,78 +255,15 @@ def get_status():
|
||||
@bp.route("/logs")
|
||||
def get_logs():
|
||||
"""Get recent activity logs with pagination support."""
|
||||
try:
|
||||
# Pagination parameters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# Legacy limit parameter for backward compatibility
|
||||
limit = request.args.get('limit', type=int)
|
||||
if limit and not request.args.get('page'):
|
||||
# Legacy mode: use limit without pagination
|
||||
logs = ActivityLog.query.order_by(ActivityLog.timestamp.desc()).limit(limit).all()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"logs": [{
|
||||
"id": log.id,
|
||||
"timestamp": log.timestamp.isoformat(),
|
||||
"action": log.action,
|
||||
"status": log.status,
|
||||
"description": log.description,
|
||||
"category": log.category
|
||||
} for log in logs]
|
||||
})
|
||||
|
||||
# Ensure reasonable per_page limits
|
||||
per_page = min(per_page, 100) # Cap at 100 items per page
|
||||
|
||||
# Build query with optional filtering
|
||||
query = ActivityLog.query
|
||||
|
||||
# Filter by categories if specified
|
||||
categories = request.args.getlist('category')
|
||||
if categories:
|
||||
query = query.filter(ActivityLog.category.in_(categories))
|
||||
|
||||
# Filter by status if specified
|
||||
status = request.args.get('status')
|
||||
if status:
|
||||
query = query.filter(ActivityLog.status == status)
|
||||
|
||||
# Order by most recent first and paginate
|
||||
pagination = query.order_by(ActivityLog.timestamp.desc()).paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
error_out=False
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"logs": [{
|
||||
"id": log.id,
|
||||
"timestamp": log.timestamp.isoformat(),
|
||||
"action": log.action,
|
||||
"status": log.status,
|
||||
"description": log.description,
|
||||
"category": log.category
|
||||
} for log in pagination.items],
|
||||
"pagination": {
|
||||
"page": pagination.page,
|
||||
"pages": pagination.pages,
|
||||
"per_page": pagination.per_page,
|
||||
"total": pagination.total,
|
||||
"has_next": pagination.has_next,
|
||||
"has_prev": pagination.has_prev,
|
||||
"next_num": pagination.next_num if pagination.has_next else None,
|
||||
"prev_num": pagination.prev_num if pagination.has_prev else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Error getting logs: {str(e)}"
|
||||
}), 500
|
||||
# Redirect to the unified logs API endpoint
|
||||
from flask import redirect, url_for
|
||||
|
||||
# Forward all query parameters to the unified endpoint
|
||||
query_string = request.query_string.decode('utf-8')
|
||||
if query_string:
|
||||
return redirect(f"{url_for('logger.get_logs_api')}?{query_string}")
|
||||
else:
|
||||
return redirect(url_for('logger.get_logs_api'))
|
||||
|
||||
@bp.route("/scrapers")
|
||||
def get_scrapers():
|
||||
|
@ -116,7 +116,7 @@ class ActivityMonitor {
|
||||
params.append("status", this.statusFilter);
|
||||
}
|
||||
|
||||
const data = await apiRequest(`/scraper/logs?${params.toString()}`);
|
||||
const data = await apiRequest(`/logs/api?${params.toString()}`);
|
||||
|
||||
if (data.success) {
|
||||
this.renderActivityLog(data.logs);
|
||||
@ -261,7 +261,7 @@ class ActivityMonitor {
|
||||
try {
|
||||
// Use the API endpoint for checking new papers, with limit for efficiency
|
||||
const data = await apiRequest(
|
||||
`/api/activity_logs?category=scraper_activity&category=scraper_command&action=scrape_paper&after=${this.lastPaperTimestamp}&limit=5`
|
||||
`/logs/api?category=scraper_activity&category=scraper_command&action=scrape_paper&after=${this.lastPaperTimestamp}&limit=5`
|
||||
);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
|
405
scipaperloader/static/js/logger-manager.js
Normal file
405
scipaperloader/static/js/logger-manager.js
Normal file
@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Logger Manager - Modern activity log management for the unified logger view
|
||||
*/
|
||||
|
||||
class LoggerManager {
|
||||
constructor(options = {}) {
|
||||
this.categories = options.categories || [];
|
||||
this.initialFilters = options.initialFilters || {};
|
||||
|
||||
// Pagination state
|
||||
this.currentPage = 1;
|
||||
this.perPage = 50;
|
||||
this.totalPages = 1;
|
||||
this.totalEntries = 0;
|
||||
|
||||
// Current filter state
|
||||
this.filters = { ...this.initialFilters };
|
||||
|
||||
// DOM elements
|
||||
this.initElements();
|
||||
this.initEventListeners();
|
||||
|
||||
// Apply initial filters and load data
|
||||
this.applyInitialFilters();
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
initElements() {
|
||||
// Form elements
|
||||
this.filtersForm = document.getElementById("logFiltersForm");
|
||||
this.categorySelect = document.getElementById("category");
|
||||
this.statusSelect = document.getElementById("status");
|
||||
this.startDateInput = document.getElementById("start_date");
|
||||
this.endDateInput = document.getElementById("end_date");
|
||||
this.searchTermInput = document.getElementById("search_term");
|
||||
this.clearFiltersBtn = document.getElementById("clearFilters");
|
||||
this.downloadLogsBtn = document.getElementById("downloadLogs");
|
||||
this.refreshLogsBtn = document.getElementById("refreshLogs");
|
||||
|
||||
// Logs display elements
|
||||
this.logsTableBody = document.getElementById("logsTableBody");
|
||||
this.pageSizeSelect = document.getElementById("logPageSize");
|
||||
|
||||
// Pagination elements
|
||||
this.paginationContainer = document.getElementById("logsPagination");
|
||||
this.paginationInfo = document.getElementById("logsPaginationInfo");
|
||||
this.prevPageBtn = document.getElementById("logsPrevPage");
|
||||
this.nextPageBtn = document.getElementById("logsNextPage");
|
||||
this.currentPageSpan = document.getElementById("logsCurrentPage");
|
||||
|
||||
// Modal
|
||||
this.logModal = new ModalHandler("logDetailModal", "log-detail-content");
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
// Filter form submission
|
||||
if (this.filtersForm) {
|
||||
this.filtersForm.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Individual filter changes for immediate application
|
||||
[
|
||||
this.categorySelect,
|
||||
this.statusSelect,
|
||||
this.startDateInput,
|
||||
this.endDateInput,
|
||||
].forEach((element) => {
|
||||
if (element) {
|
||||
element.addEventListener("change", () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search term with debounce
|
||||
if (this.searchTermInput) {
|
||||
let searchTimeout;
|
||||
this.searchTermInput.addEventListener("input", () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.applyFilters();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Clear filters
|
||||
if (this.clearFiltersBtn) {
|
||||
this.clearFiltersBtn.addEventListener("click", () => {
|
||||
this.clearAllFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Download logs
|
||||
if (this.downloadLogsBtn) {
|
||||
this.downloadLogsBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.downloadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh logs
|
||||
if (this.refreshLogsBtn) {
|
||||
this.refreshLogsBtn.addEventListener("click", () => {
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
// Page size change
|
||||
if (this.pageSizeSelect) {
|
||||
this.pageSizeSelect.addEventListener("change", () => {
|
||||
this.perPage = parseInt(this.pageSizeSelect.value);
|
||||
this.currentPage = 1; // Reset to first page
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination buttons
|
||||
if (this.prevPageBtn) {
|
||||
this.prevPageBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.nextPageBtn) {
|
||||
this.nextPageBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++;
|
||||
this.loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyInitialFilters() {
|
||||
// Set form values from initial filters
|
||||
if (this.categorySelect && this.initialFilters.category) {
|
||||
this.categorySelect.value = this.initialFilters.category;
|
||||
}
|
||||
if (this.startDateInput && this.initialFilters.start_date) {
|
||||
this.startDateInput.value = this.initialFilters.start_date;
|
||||
}
|
||||
if (this.endDateInput && this.initialFilters.end_date) {
|
||||
this.endDateInput.value = this.initialFilters.end_date;
|
||||
}
|
||||
if (this.searchTermInput && this.initialFilters.search_term) {
|
||||
this.searchTermInput.value = this.initialFilters.search_term;
|
||||
}
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
// Collect current filter values
|
||||
this.filters = {
|
||||
category: this.categorySelect?.value || "",
|
||||
status: this.statusSelect?.value || "",
|
||||
start_date: this.startDateInput?.value || "",
|
||||
end_date: this.endDateInput?.value || "",
|
||||
search_term: this.searchTermInput?.value || "",
|
||||
};
|
||||
|
||||
// Reset to first page when filters change
|
||||
this.currentPage = 1;
|
||||
|
||||
// Load logs with new filters
|
||||
this.loadLogs();
|
||||
|
||||
// Update URL to reflect current filters (for bookmarking/sharing)
|
||||
this.updateUrl();
|
||||
}
|
||||
|
||||
clearAllFilters() {
|
||||
// Clear all form fields
|
||||
if (this.categorySelect) this.categorySelect.value = "";
|
||||
if (this.statusSelect) this.statusSelect.value = "";
|
||||
if (this.startDateInput) this.startDateInput.value = "";
|
||||
if (this.endDateInput) this.endDateInput.value = "";
|
||||
if (this.searchTermInput) this.searchTermInput.value = "";
|
||||
|
||||
// Apply empty filters
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
async loadLogs() {
|
||||
if (!this.logsTableBody) return;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
this.logsTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center">Loading logs...</td></tr>';
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
page: this.currentPage,
|
||||
per_page: this.perPage,
|
||||
});
|
||||
|
||||
// Add filters to query
|
||||
Object.entries(this.filters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch logs from unified API
|
||||
const data = await apiRequest(`/logs/api?${params.toString()}`);
|
||||
|
||||
if (data.success) {
|
||||
this.renderLogs(data.logs);
|
||||
this.updatePagination(data.pagination);
|
||||
console.log("Logs loaded successfully");
|
||||
} else {
|
||||
throw new Error(data.message || "Failed to load logs");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load logs:", error);
|
||||
this.logsTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center text-danger">Error loading logs. Please try again.</td></tr>';
|
||||
this.hidePagination();
|
||||
}
|
||||
}
|
||||
|
||||
renderLogs(logs) {
|
||||
if (!this.logsTableBody) return;
|
||||
|
||||
this.logsTableBody.innerHTML = "";
|
||||
|
||||
if (!logs || logs.length === 0) {
|
||||
this.logsTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center">No logs found matching the current filters.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
logs.forEach((log) => {
|
||||
const row = document.createElement("tr");
|
||||
row.className = "log-item";
|
||||
row.setAttribute("data-log-id", log.id);
|
||||
|
||||
// Format timestamp
|
||||
const timeStr = formatTimestamp(log.timestamp);
|
||||
|
||||
// Create status badge
|
||||
const statusBadge = createStatusBadge(log.status);
|
||||
|
||||
// Create category badge
|
||||
const categoryBadge = this.createCategoryBadge(log.category);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${timeStr}</td>
|
||||
<td>${categoryBadge}</td>
|
||||
<td>${log.action}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${log.description || ""}</td>
|
||||
`;
|
||||
|
||||
// Add click handler for details modal
|
||||
row.addEventListener("click", () => {
|
||||
const url = `/logs/${log.id}/detail`;
|
||||
this.logModal.loadAndShow(url, "Error loading log details.");
|
||||
});
|
||||
|
||||
this.logsTableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
createCategoryBadge(category) {
|
||||
const categoryColors = {
|
||||
gui_interaction: "bg-primary",
|
||||
config_change: "bg-warning",
|
||||
scraper_command: "bg-info",
|
||||
scraper_activity: "bg-success",
|
||||
system: "bg-danger",
|
||||
data_import: "bg-secondary",
|
||||
};
|
||||
|
||||
const colorClass = categoryColors[category] || "bg-secondary";
|
||||
const displayName = category
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
return `<span class="badge ${colorClass}">${displayName}</span>`;
|
||||
}
|
||||
|
||||
updatePagination(pagination) {
|
||||
if (!pagination || !this.paginationContainer) return;
|
||||
|
||||
this.currentPage = pagination.page;
|
||||
this.totalPages = pagination.pages;
|
||||
this.totalEntries = pagination.total;
|
||||
|
||||
// Show pagination container
|
||||
this.paginationContainer.classList.remove("d-none");
|
||||
|
||||
// Update pagination info
|
||||
const startEntry = (pagination.page - 1) * pagination.per_page + 1;
|
||||
const endEntry = Math.min(
|
||||
pagination.page * pagination.per_page,
|
||||
pagination.total
|
||||
);
|
||||
|
||||
if (this.paginationInfo) {
|
||||
this.paginationInfo.textContent = `Showing ${startEntry} - ${endEntry} of ${pagination.total} entries`;
|
||||
}
|
||||
|
||||
// Update current page display
|
||||
if (this.currentPageSpan) {
|
||||
this.currentPageSpan.innerHTML = `<span class="page-link">${pagination.page} of ${pagination.pages}</span>`;
|
||||
}
|
||||
|
||||
// Update previous button
|
||||
if (this.prevPageBtn) {
|
||||
if (pagination.has_prev) {
|
||||
this.prevPageBtn.classList.remove("disabled");
|
||||
this.prevPageBtn.querySelector("a").removeAttribute("tabindex");
|
||||
this.prevPageBtn
|
||||
.querySelector("a")
|
||||
.setAttribute("aria-disabled", "false");
|
||||
} else {
|
||||
this.prevPageBtn.classList.add("disabled");
|
||||
this.prevPageBtn.querySelector("a").setAttribute("tabindex", "-1");
|
||||
this.prevPageBtn
|
||||
.querySelector("a")
|
||||
.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
}
|
||||
|
||||
// Update next button
|
||||
if (this.nextPageBtn) {
|
||||
if (pagination.has_next) {
|
||||
this.nextPageBtn.classList.remove("disabled");
|
||||
this.nextPageBtn.querySelector("a").removeAttribute("tabindex");
|
||||
this.nextPageBtn
|
||||
.querySelector("a")
|
||||
.setAttribute("aria-disabled", "false");
|
||||
} else {
|
||||
this.nextPageBtn.classList.add("disabled");
|
||||
this.nextPageBtn.querySelector("a").setAttribute("tabindex", "-1");
|
||||
this.nextPageBtn
|
||||
.querySelector("a")
|
||||
.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hidePagination() {
|
||||
if (this.paginationContainer) {
|
||||
this.paginationContainer.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
// Update URL with current filters for bookmarking
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.entries(this.filters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const newUrl = `${window.location.pathname}${
|
||||
params.toString() ? "?" + params.toString() : ""
|
||||
}`;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
}
|
||||
|
||||
downloadLogs() {
|
||||
// Build download URL with current filters
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.entries(this.filters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const downloadUrl = `/logs/download${
|
||||
params.toString() ? "?" + params.toString() : ""
|
||||
}`;
|
||||
window.location.href = downloadUrl;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set modal handler for log details
|
||||
* @param {ModalHandler} modalHandler - Modal handler instance
|
||||
*/
|
||||
setModalHandler(modalHandler) {
|
||||
this.logModal = modalHandler;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof window !== "undefined") {
|
||||
window.LoggerManager = LoggerManager;
|
||||
}
|
@ -10,6 +10,9 @@ class ModalHandler {
|
||||
|
||||
if (this.modalElement && typeof bootstrap !== "undefined") {
|
||||
this.modal = new bootstrap.Modal(this.modalElement);
|
||||
|
||||
// Set up global event delegation for modal close buttons
|
||||
this.setupGlobalCloseHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +31,13 @@ class ModalHandler {
|
||||
const response = await fetch(url);
|
||||
const html = await response.text();
|
||||
this.contentElement.innerHTML = html;
|
||||
|
||||
// Set up close button handlers after content is loaded
|
||||
this.setupCloseHandlers();
|
||||
|
||||
// Format any JSON content in the modal
|
||||
this.formatJsonContent();
|
||||
|
||||
this.modal.show();
|
||||
} catch (error) {
|
||||
console.error("Error loading modal content:", error);
|
||||
@ -63,9 +73,143 @@ class ModalHandler {
|
||||
if (!this.modal || !this.contentElement) return;
|
||||
|
||||
this.contentElement.innerHTML = content;
|
||||
|
||||
// Set up close button handlers after content is loaded
|
||||
this.setupCloseHandlers();
|
||||
|
||||
this.modal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up global event delegation for modal close buttons
|
||||
*/
|
||||
setupGlobalCloseHandlers() {
|
||||
// Use event delegation to handle dynamically loaded close buttons
|
||||
this.modalElement.addEventListener("click", (e) => {
|
||||
if (
|
||||
e.target.matches('[data-bs-dismiss="modal"]') ||
|
||||
e.target.closest('[data-bs-dismiss="modal"]') ||
|
||||
e.target.matches(".btn-close") ||
|
||||
e.target.closest(".btn-close")
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle ESC key press
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
this.modal &&
|
||||
this.modalElement.classList.contains("show")
|
||||
) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up close button event handlers for dynamically loaded content
|
||||
*/
|
||||
setupCloseHandlers() {
|
||||
// This method is now mostly redundant due to global event delegation
|
||||
// but we'll keep it for backward compatibility
|
||||
|
||||
// Handle close buttons with data-bs-dismiss="modal"
|
||||
const closeButtons = this.contentElement.querySelectorAll(
|
||||
'[data-bs-dismiss="modal"]'
|
||||
);
|
||||
closeButtons.forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle close buttons with .btn-close class
|
||||
const closeButtonsClass =
|
||||
this.contentElement.querySelectorAll(".btn-close");
|
||||
closeButtonsClass.forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
});
|
||||
});
|
||||
|
||||
// Also handle ESC key press
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
this.modal &&
|
||||
this.modalElement.classList.contains("show")
|
||||
) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format JSON content in the modal after it's loaded
|
||||
*/
|
||||
formatJsonContent() {
|
||||
// Format JSON in extra data if present
|
||||
const extraDataElement = this.contentElement.querySelector(
|
||||
"#extra-data-content"
|
||||
);
|
||||
if (extraDataElement && extraDataElement.textContent.trim()) {
|
||||
try {
|
||||
const jsonData = JSON.parse(extraDataElement.textContent);
|
||||
|
||||
// Pretty-format the JSON with proper indentation
|
||||
const formattedJson = JSON.stringify(jsonData, null, 2);
|
||||
extraDataElement.textContent = formattedJson;
|
||||
|
||||
// Add syntax highlighting classes if the JSON is complex
|
||||
if (typeof jsonData === "object" && jsonData !== null) {
|
||||
extraDataElement.parentElement.classList.add("json-formatted");
|
||||
}
|
||||
} catch (e) {
|
||||
// If it's not valid JSON, leave it as is but still format if it looks like JSON
|
||||
const text = extraDataElement.textContent.trim();
|
||||
if (text.startsWith("{") || text.startsWith("[")) {
|
||||
// Try to fix common JSON issues and reformat
|
||||
try {
|
||||
const fixedJson = text
|
||||
.replace(/'/g, '"')
|
||||
.replace(/None/g, "null")
|
||||
.replace(/True/g, "true")
|
||||
.replace(/False/g, "false");
|
||||
const parsed = JSON.parse(fixedJson);
|
||||
extraDataElement.textContent = JSON.stringify(parsed, null, 2);
|
||||
} catch (fixError) {
|
||||
// If still can't parse, just leave as is
|
||||
console.debug("Extra data is not valid JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also format old_value and new_value if they contain JSON
|
||||
const preElements = this.contentElement.querySelectorAll("pre code");
|
||||
preElements.forEach(function (codeElement) {
|
||||
if (codeElement && codeElement.textContent.trim()) {
|
||||
const text = codeElement.textContent.trim();
|
||||
if (
|
||||
(text.startsWith("{") && text.endsWith("}")) ||
|
||||
(text.startsWith("[") && text.endsWith("]"))
|
||||
) {
|
||||
try {
|
||||
const jsonData = JSON.parse(text);
|
||||
codeElement.textContent = JSON.stringify(jsonData, null, 2);
|
||||
} catch (e) {
|
||||
// Not JSON, leave as is
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the modal
|
||||
*/
|
||||
|
@ -7,3 +7,34 @@
|
||||
.progress-bar {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
/* JSON formatting styles */
|
||||
.json-formatted {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.json-formatted code {
|
||||
color: #212529;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Improve readability of JSON in modals */
|
||||
#extra-data-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Style for old/new value code blocks */
|
||||
pre code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
@ -1,120 +0,0 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block title %}Logs{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Activity Logs</h1>
|
||||
|
||||
<!-- Include flash messages template -->
|
||||
{% include "partials/flash_messages.html.jinja" %}
|
||||
|
||||
<form method="get" class="mb-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label for="category" class="form-label">Category:</label>
|
||||
<select name="category" id="category" class="form-select">
|
||||
<option value="">All</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat }}" {% if category==cat %}selected{% endif %}>{{ cat }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="start_date" class="form-label">Start Date:</label>
|
||||
<input type="date" name="start_date" id="start_date" value="{{ start_date }}" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="end_date" class="form-label">End Date:</label>
|
||||
<input type="date" name="end_date" id="end_date" value="{{ end_date }}" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="search_term" class="form-label">Search:</label>
|
||||
<input type="text" name="search_term" id="search_term" value="{{ search_term }}" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary">Filter</button>
|
||||
<a href="{{ url_for('logger.download_logs', category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}"
|
||||
class="btn btn-secondary">Download CSV</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item log-item" data-log-id="{{ log.id }}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">{{ log.timestamp }}</div>
|
||||
{{ log.action }} - {{ log.description }}
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{ log.category }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if pagination %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('logger.list_logs', page=pagination.prev_num, category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page {{ pagination.page }} of {{ pagination.pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('logger.list_logs', page=pagination.next_num, category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Modal for log details -->
|
||||
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content" id="log-detail-content">
|
||||
<!-- Log details will be loaded here via AJAX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/modal-handler.js') }}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Use the reusable ModalHandler for log details
|
||||
const logModal = new ModalHandler('logDetailModal', 'log-detail-content');
|
||||
|
||||
// Set up click handlers for log items with custom URL construction
|
||||
document.querySelectorAll('.log-item').forEach(item => {
|
||||
item.addEventListener('click', function () {
|
||||
const logId = this.getAttribute('data-log-id');
|
||||
const url = `/logs/${logId}/detail`;
|
||||
logModal.loadAndShow(url, 'Error loading log details.');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}
|
240
scipaperloader/templates/logs.html.jinja
Normal file
240
scipaperloader/templates/logs.html.jinja
Normal file
@ -0,0 +1,240 @@
|
||||
{% extends "base.html.jinja" %}
|
||||
|
||||
{% block title %}Activity Logs{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.logs-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.activity-controls {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* JSON formatting styles */
|
||||
.json-formatted {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.375rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.json-formatted code {
|
||||
color: #495057;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<h1><i class="fas fa-list-alt"></i> Activity Logs</h1>
|
||||
|
||||
<!-- Include standardized flash messages -->
|
||||
{% include "partials/flash_messages.html.jinja" %}
|
||||
|
||||
<div class="logs-container">
|
||||
<!-- Filter Panel -->
|
||||
<div class="filter-panel">
|
||||
<form id="filterForm" class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label for="categoryFilter" class="form-label">Category:</label>
|
||||
<select id="categoryFilter" class="form-select form-select-sm">
|
||||
<option value="">All Categories</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat }}" {% if category==cat %}selected{% endif %}>{{ cat.replace('_', '
|
||||
').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="statusFilter" class="form-label">Status:</label>
|
||||
<select id="statusFilter" class="form-select form-select-sm">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="startDate" class="form-label">Start Date:</label>
|
||||
<input type="date" id="startDate" class="form-control form-control-sm"
|
||||
value="{{ start_date or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label for="endDate" class="form-label">End Date:</label>
|
||||
<input type="date" id="endDate" class="form-control form-control-sm" value="{{ end_date or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="searchTerm" class="form-label">Search:</label>
|
||||
<input type="text" id="searchTerm" class="form-control form-control-sm"
|
||||
placeholder="Search action or description..." value="{{ search_term or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" id="applyFilters" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
<button type="button" id="clearFilters" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Controls Panel -->
|
||||
<div class="d-flex justify-content-between align-items-center p-3 border-bottom">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="form-group mb-0">
|
||||
<label for="pageSize" class="form-label mb-0 me-2">Show:</label>
|
||||
<select id="pageSize" class="form-select form-select-sm activity-controls">
|
||||
<option value="20">20</option>
|
||||
<option value="50" selected>50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<span id="paginationInfo" class="pagination-info">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" id="refreshLogs" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-sync-alt"></i> Refresh
|
||||
</button>
|
||||
<button type="button" id="downloadLogs" class="btn btn-outline-success btn-sm">
|
||||
<i class="fas fa-download"></i> Download CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Table -->
|
||||
<div class="search-results-container">
|
||||
<table class="table table-hover logs-table mb-0">
|
||||
<thead class="sticky-top">
|
||||
<tr>
|
||||
<th style="width: 150px;">Timestamp</th>
|
||||
<th style="width: 120px;">Category</th>
|
||||
<th style="width: 180px;">Action</th>
|
||||
<th style="width: 100px;">Status</th>
|
||||
<th>Description</th>
|
||||
<th style="width: 60px;">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsTableBody">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading logs...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<nav id="logsPagination" aria-label="Logs pagination" class="p-3 border-top d-none">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="pagination-info">
|
||||
<span id="paginationDetails">Showing 0 - 0 of 0 entries</span>
|
||||
</div>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item" id="prevPage">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item active" id="currentPageItem">
|
||||
<span class="page-link" id="currentPageSpan">1</span>
|
||||
</li>
|
||||
<li class="page-item" id="nextPage">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for log details -->
|
||||
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="true"
|
||||
data-bs-keyboard="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content" id="log-detail-content">
|
||||
<!-- Log details will be loaded here via AJAX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/modal-handler.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/logger-manager.js') }}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Initialize the logger manager
|
||||
window.loggerManager = new LoggerManager({
|
||||
initialCategory: "{{ category or '' }}",
|
||||
initialStartDate: "{{ start_date or '' }}",
|
||||
initialEndDate: "{{ end_date or '' }}",
|
||||
initialSearchTerm: "{{ search_term or '' }}"
|
||||
});
|
||||
|
||||
// Set up modal handler for log details
|
||||
const logModal = new ModalHandler('logDetailModal', 'log-detail-content');
|
||||
window.loggerManager.setModalHandler(logModal);
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}
|
@ -1,18 +1,82 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Log Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title"><i class="fas fa-info-circle"></i> Log Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Timestamp:</strong> {{ log.timestamp }}</p>
|
||||
<p><strong>Category:</strong> {{ log.category }}</p>
|
||||
<p><strong>Action:</strong> {{ log.action }}</p>
|
||||
<p><strong>Description:</strong> {{ log.description }}</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Timestamp:</strong> <span class="text-muted">{{ log.timestamp }}</span></p>
|
||||
<p><strong>Category:</strong>
|
||||
<span class="badge bg-secondary">{{ log.category.replace('_', ' ').title() }}</span>
|
||||
</p>
|
||||
<p><strong>Action:</strong> <code>{{ log.action }}</code></p>
|
||||
{% if log.status %}
|
||||
<p><strong>Status:</strong>
|
||||
{% if log.status == 'success' %}
|
||||
<span class="badge bg-success">{{ log.status.title() }}</span>
|
||||
{% elif log.status == 'error' %}
|
||||
<span class="badge bg-danger">{{ log.status.title() }}</span>
|
||||
{% elif log.status == 'warning' %}
|
||||
<span class="badge bg-warning">{{ log.status.title() }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">{{ log.status.title() }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if log.paper_id %}
|
||||
<p><strong>Paper ID:</strong> <a href="/papers/{{ log.paper_id }}" target="_blank">{{ log.paper_id }}</a></p>
|
||||
{% endif %}
|
||||
{% if log.user_id %}
|
||||
<p><strong>User ID:</strong> {{ log.user_id }}</p>
|
||||
{% endif %}
|
||||
{% if log.config_key %}
|
||||
<p><strong>Config Key:</strong> <code>{{ log.config_key }}</code></p>
|
||||
{% endif %}
|
||||
{% if log.source_ip %}
|
||||
<p><strong>Source IP:</strong> {{ log.source_ip }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if log.description %}
|
||||
<div class="mt-3">
|
||||
<p><strong>Description:</strong></p>
|
||||
<div class="alert alert-light">{{ log.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if log.old_value or log.new_value %}
|
||||
<div class="mt-3">
|
||||
<p><strong>Configuration Changes:</strong></p>
|
||||
<div class="row">
|
||||
{% if log.old_value %}
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><strong>Old Value:</strong></label>
|
||||
<pre class="bg-light p-2"><code>{{ log.old_value }}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if log.new_value %}
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><strong>New Value:</strong></label>
|
||||
<pre class="bg-light p-2"><code>{{ log.new_value }}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if log.extra_data %}
|
||||
<p><strong>Extra Data:</strong>
|
||||
<pre><code>{{ log.extra_data }}</code></pre>
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
<p><strong>Additional Data:</strong></p>
|
||||
<pre class="bg-light p-3"
|
||||
style="max-height: 300px; overflow-y: auto;"><code id="extra-data-content">{{ log.extra_data }}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times"></i> Close
|
||||
</button>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user