/** * Activity monitoring and display functionality */ class ActivityMonitor { constructor() { this.activityLog = document.getElementById("activityLog"); this.notificationsToggle = document.getElementById("notificationsToggle"); this.notificationsEnabled = true; this.lastPaperTimestamp = new Date().toISOString(); // Pagination state this.currentPage = 1; this.perPage = 20; this.statusFilter = ""; this.totalPages = 1; this.totalEntries = 0; // Pagination elements this.paginationContainer = document.getElementById("activityPagination"); this.paginationInfo = document.getElementById("activityPaginationInfo"); this.prevPageBtn = document.getElementById("activityPrevPage"); this.nextPageBtn = document.getElementById("activityNextPage"); this.currentPageSpan = document.getElementById("activityCurrentPage"); this.pageSizeSelect = document.getElementById("activityPageSize"); this.statusFilterSelect = document.getElementById("activityStatusFilter"); this.initEventListeners(); this.setupWebSocket(); } /** * Initialize event listeners */ initEventListeners() { if (this.notificationsToggle) { this.notificationsToggle.addEventListener("click", () => { this.notificationsEnabled = this.notificationsToggle.checked; }); } // Time range buttons document.querySelectorAll(".time-range-btn").forEach((btn) => { btn.addEventListener("click", () => { document .querySelectorAll(".time-range-btn") .forEach((b) => b.classList.remove("active")); btn.classList.add("active"); const currentTimeRange = parseInt(btn.dataset.hours); // Trigger chart refresh if callback is provided if (this.onChartRefresh) { this.onChartRefresh(currentTimeRange); } }); }); // Pagination event listeners if (this.prevPageBtn) { this.prevPageBtn.addEventListener("click", (e) => { e.preventDefault(); if (this.currentPage > 1) { this.currentPage--; this.loadRecentActivity(); } }); } if (this.nextPageBtn) { this.nextPageBtn.addEventListener("click", (e) => { e.preventDefault(); if (this.currentPage < this.totalPages) { this.currentPage++; this.loadRecentActivity(); } }); } // Page size change if (this.pageSizeSelect) { this.pageSizeSelect.addEventListener("change", () => { this.perPage = parseInt(this.pageSizeSelect.value); this.currentPage = 1; // Reset to first page this.loadRecentActivity(); }); } // Status filter change if (this.statusFilterSelect) { this.statusFilterSelect.addEventListener("change", () => { this.statusFilter = this.statusFilterSelect.value; this.currentPage = 1; // Reset to first page this.loadRecentActivity(); }); } } /** * Load and render recent activity */ async loadRecentActivity() { if (!this.activityLog) return; try { // Build query parameters for pagination const params = new URLSearchParams({ page: this.currentPage, per_page: this.perPage, }); // Add multiple category parameters params.append("category", "scraper_activity"); params.append("category", "scraper_command"); if (this.statusFilter) { params.append("status", this.statusFilter); } const data = await apiRequest(`/logs/api?${params.toString()}`); if (data.success) { this.renderActivityLog(data.logs); this.updatePagination(data.pagination); console.log("Activity log refreshed with latest data"); } else { throw new Error(data.message || "Failed to load logs"); } } catch (error) { console.error("Failed to load activity logs:", error); // If the API endpoint doesn't exist, just show a message this.activityLog.innerHTML = 'Activity log API not available'; this.hidePagination(); } } /** * Render activity log data * @param {Array} logs - Array of log entries */ renderActivityLog(logs) { if (!this.activityLog) return; this.activityLog.innerHTML = ""; if (!logs || logs.length === 0) { this.activityLog.innerHTML = 'No recent activity'; return; } logs.forEach((log) => { const row = document.createElement("tr"); // Format timestamp const timeStr = formatTimestamp(log.timestamp); // Create status badge const statusBadge = createStatusBadge(log.status); row.innerHTML = ` ${timeStr} ${log.action} ${statusBadge} ${log.description || ""} `; this.activityLog.appendChild(row); }); } /** * Update pagination controls based on API response * @param {Object} pagination - Pagination data from API */ 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.textContent = `${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"); } } } /** * Hide pagination controls when not needed */ hidePagination() { if (this.paginationContainer) { this.paginationContainer.classList.add("d-none"); } } /** * Setup WebSocket for real-time notifications */ setupWebSocket() { // If WebSocket is available, implement it here // For now we'll poll the server periodically for new papers setInterval(() => this.checkForNewPapers(), 10000); // Check every 10 seconds } /** * Check for new papers and show notifications */ async checkForNewPapers() { if (!this.notificationsEnabled) return; try { // Use the API endpoint for checking new papers, with limit for efficiency const data = await apiRequest( `/logs/api?category=scraper_activity&category=scraper_command&action=scrape_paper&after=${this.lastPaperTimestamp}&limit=5` ); if (data && data.length > 0) { // Update the timestamp this.lastPaperTimestamp = new Date().toISOString(); // Show notifications for new papers data.forEach((log) => { const extraData = log.extra_data ? JSON.parse(log.extra_data) : {}; if (log.status === "success") { showFlashMessage( `New paper scraped: ${extraData.title || "Unknown title"}`, "success" ); } else if (log.status === "error") { showFlashMessage( `Failed to scrape paper: ${log.description}`, "error" ); } }); // Refresh the activity chart and log if (this.onChartRefresh) { this.onChartRefresh(); } // Only reload if we're on page 1 to avoid disrupting user navigation if (this.currentPage === 1) { this.loadRecentActivity(); } } } catch (error) { // If the API endpoint doesn't exist, do nothing console.debug("Activity polling failed (this may be expected):", error); } } /** * Set callback for chart refresh */ setChartRefreshCallback(callback) { this.onChartRefresh = callback; } /** * Refresh activity log manually (useful for external triggers) */ refresh() { this.loadRecentActivity(); } /** * Reset pagination to first page */ resetToFirstPage() { this.currentPage = 1; this.loadRecentActivity(); } } // Export for use in other modules if (typeof window !== "undefined") { window.ActivityMonitor = ActivityMonitor; }