diff --git a/scipaperloader/blueprints/logger.py b/scipaperloader/blueprints/logger.py index 3976b1e..2f8e7ea 100644 --- a/scipaperloader/blueprints/logger.py +++ b/scipaperloader/blueprints/logger.py @@ -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("//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) \ No newline at end of file + 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 \ No newline at end of file diff --git a/scipaperloader/blueprints/scraper.py b/scipaperloader/blueprints/scraper.py index 95d5947..4db6f6a 100644 --- a/scipaperloader/blueprints/scraper.py +++ b/scipaperloader/blueprints/scraper.py @@ -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(): diff --git a/scipaperloader/static/js/activity-monitor.js b/scipaperloader/static/js/activity-monitor.js index 8831f29..be7818f 100644 --- a/scipaperloader/static/js/activity-monitor.js +++ b/scipaperloader/static/js/activity-monitor.js @@ -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) { diff --git a/scipaperloader/static/js/logger-manager.js b/scipaperloader/static/js/logger-manager.js new file mode 100644 index 0000000..d32e47a --- /dev/null +++ b/scipaperloader/static/js/logger-manager.js @@ -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 = + 'Loading logs...'; + + // 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 = + 'Error loading logs. Please try again.'; + this.hidePagination(); + } + } + + renderLogs(logs) { + if (!this.logsTableBody) return; + + this.logsTableBody.innerHTML = ""; + + if (!logs || logs.length === 0) { + this.logsTableBody.innerHTML = + 'No logs found matching the current filters.'; + 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 = ` + ${timeStr} + ${categoryBadge} + ${log.action} + ${statusBadge} + ${log.description || ""} + `; + + // 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 `${displayName}`; + } + + 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 = `${pagination.page} of ${pagination.pages}`; + } + + // 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; +} diff --git a/scipaperloader/static/js/modal-handler.js b/scipaperloader/static/js/modal-handler.js index df0db33..6c789ba 100644 --- a/scipaperloader/static/js/modal-handler.js +++ b/scipaperloader/static/js/modal-handler.js @@ -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 */ diff --git a/scipaperloader/static/styles.css b/scipaperloader/static/styles.css index 048611a..dcd3c3c 100644 --- a/scipaperloader/static/styles.css +++ b/scipaperloader/static/styles.css @@ -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; +} diff --git a/scipaperloader/templates/logger.html.jinja b/scipaperloader/templates/logger.html.jinja deleted file mode 100644 index 758955e..0000000 --- a/scipaperloader/templates/logger.html.jinja +++ /dev/null @@ -1,120 +0,0 @@ -{% extends "base.html.jinja" %} - -{% block title %}Logs{% endblock title %} - -{% block content %} -

Activity Logs

- - -{% include "partials/flash_messages.html.jinja" %} - -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- - Download CSV -
-
- - - -{% if pagination %} - -{% endif %} - - - -{% endblock content %} - -{% block scripts %} -{{ super() }} - - -{% endblock scripts %} \ No newline at end of file diff --git a/scipaperloader/templates/logs.html.jinja b/scipaperloader/templates/logs.html.jinja new file mode 100644 index 0000000..7ff7e7d --- /dev/null +++ b/scipaperloader/templates/logs.html.jinja @@ -0,0 +1,240 @@ +{% extends "base.html.jinja" %} + +{% block title %}Activity Logs{% endblock title %} + +{% block styles %} +{{ super() }} + +{% endblock styles %} + +{% block content %} +
+

Activity Logs

+ + + {% include "partials/flash_messages.html.jinja" %} + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+ + +
+
+
+ + +
+ Loading... +
+ +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + +
TimestampCategoryActionStatusDescriptionDetails
+ Loading logs... +
+
+ + + +
+
+ + + +{% endblock content %} + +{% block scripts %} +{{ super() }} + + + + +{% endblock scripts %} \ No newline at end of file diff --git a/scipaperloader/templates/partials/log_detail_modal.html.jinja b/scipaperloader/templates/partials/log_detail_modal.html.jinja index 8fd4a0d..d5ae633 100644 --- a/scipaperloader/templates/partials/log_detail_modal.html.jinja +++ b/scipaperloader/templates/partials/log_detail_modal.html.jinja @@ -1,18 +1,82 @@ \ No newline at end of file