486 lines
14 KiB
JavaScript

/**
* 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("filterForm");
this.categoryCheckboxes = document.querySelectorAll(".category-checkbox");
this.selectAllCategories = document.getElementById("selectAllCategories");
this.statusSelect = document.getElementById("statusFilter");
this.startDateInput = document.getElementById("startDate");
this.endDateInput = document.getElementById("endDate");
this.searchTermInput = document.getElementById("searchTerm");
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("pageSize");
// Pagination elements
this.paginationContainer = document.getElementById("logsPagination");
this.paginationInfo = document.getElementById("paginationDetails");
this.prevPageBtn = document.getElementById("prevPage");
this.nextPageBtn = document.getElementById("nextPage");
this.currentPageSpan = document.getElementById("currentPageSpan");
// 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();
});
}
// Handle "Select All" checkbox for categories
if (this.selectAllCategories) {
this.selectAllCategories.addEventListener("change", () => {
const isChecked = this.selectAllCategories.checked;
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
});
this.applyFilters();
});
}
// Handle individual category checkboxes
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
// Update "Select All" checkbox state
this.updateSelectAllState();
this.applyFilters();
});
});
// Individual filter changes for immediate application
[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();
}
});
}
}
updateSelectAllState() {
const checkedCount = Array.from(this.categoryCheckboxes).filter(
(cb) => cb.checked
).length;
const totalCount = this.categoryCheckboxes.length;
if (checkedCount === 0) {
this.selectAllCategories.checked = false;
this.selectAllCategories.indeterminate = false;
} else if (checkedCount === totalCount) {
this.selectAllCategories.checked = true;
this.selectAllCategories.indeterminate = false;
} else {
this.selectAllCategories.checked = false;
this.selectAllCategories.indeterminate = true;
}
}
getSelectedCategories() {
return Array.from(this.categoryCheckboxes)
.filter((checkbox) => checkbox.checked)
.map((checkbox) => checkbox.value);
}
applyInitialFilters() {
// Set category checkboxes from initial filters
if (this.initialFilters.category) {
const selectedCategories = Array.isArray(this.initialFilters.category)
? this.initialFilters.category
: [this.initialFilters.category];
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = selectedCategories.includes(checkbox.value);
});
this.updateSelectAllState();
}
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
const selectedCategories = this.getSelectedCategories();
this.filters = {
category: selectedCategories, // Now an array
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 category checkboxes and select all
this.categoryCheckboxes.forEach((checkbox) => {
checkbox.checked = true; // Default to all selected
});
if (this.selectAllCategories) {
this.selectAllCategories.checked = true;
this.selectAllCategories.indeterminate = false;
}
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"><div class="spinner-border spinner-border-sm text-primary" role="status"><span class="visually-hidden">Loading...</span></div> 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) {
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else 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-entry";
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 - whole row is clickable
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) {
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else 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) {
if (key === "category" && Array.isArray(value)) {
// Handle multiple categories
value.forEach((cat) => {
if (cat) params.append("category", cat);
});
} else 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;
}