329 lines
9.5 KiB
JavaScript
329 lines
9.5 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<tr><td colspan="4" class="text-center">Activity log API not available</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="4" class="text-center">No recent activity</td></tr>';
|
|
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 = `
|
|
<td>${timeStr}</td>
|
|
<td>${log.action}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${log.description || ""}</td>
|
|
`;
|
|
|
|
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;
|
|
}
|