/**
* 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(`/scraper/logs?${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(
`/api/activity_logs?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;
}