From 6b588ba27c53db52791da926e6620d1bca7018cf Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Wed, 4 Feb 2026 21:50:15 +0100 Subject: [PATCH] fix: add missing imports.js and background-tasks.js to marketplace module Copy JS files from monitoring to marketplace static folder to match the template locations. Co-Authored-By: Claude Opus 4.5 --- .../static/admin/js/background-tasks.js | 137 +++++ .../marketplace/static/admin/js/imports.js | 468 ++++++++++++++++++ 2 files changed, 605 insertions(+) create mode 100644 app/modules/marketplace/static/admin/js/background-tasks.js create mode 100644 app/modules/marketplace/static/admin/js/imports.js diff --git a/app/modules/marketplace/static/admin/js/background-tasks.js b/app/modules/marketplace/static/admin/js/background-tasks.js new file mode 100644 index 00000000..4834b449 --- /dev/null +++ b/app/modules/marketplace/static/admin/js/background-tasks.js @@ -0,0 +1,137 @@ +// noqa: js-006 - async init pattern is safe, loadData has try/catch +/** + * Background Tasks Monitoring Component + * Manages the background tasks monitoring page + */ + +// Use centralized logger +const backgroundTasksLog = window.LogConfig.createLogger('BACKGROUND-TASKS'); + +function backgroundTasks() { + return { + // Extend base data + ...data(), + + // Set current page for navigation + currentPage: 'background-tasks', + + // Page-specific data + loading: false, + error: null, + filterType: null, + pollInterval: null, + + // Statistics + stats: { + total_tasks: 0, + running: 0, + completed: 0, + failed: 0, + tasks_today: 0, + avg_duration_seconds: null, + import_jobs: {}, + test_runs: {} + }, + + // Tasks + tasks: [], + runningTasks: [], + + async init() { + // Guard against multiple initialization + if (window._adminBackgroundTasksInitialized) return; + window._adminBackgroundTasksInitialized = true; + + try { + backgroundTasksLog.info('Initializing background tasks monitor'); + await this.loadStats(); + await this.loadTasks(); + await this.loadRunningTasks(); + + // Poll for updates every 5 seconds + this.pollInterval = setInterval(() => { + this.loadRunningTasks(); + if (this.runningTasks.length > 0) { + this.loadStats(); + } + }, 5000); + } catch (error) { + backgroundTasksLog.error('Failed to initialize background tasks:', error); + } + }, + + destroy() { + if (this.pollInterval) { + clearInterval(this.pollInterval); + } + }, + + async loadStats() { + try { + const stats = await apiClient.get('/admin/background-tasks/tasks/stats'); + this.stats = stats; + backgroundTasksLog.info('Stats loaded:', stats); + } catch (err) { + backgroundTasksLog.error('Failed to load stats:', err); + } + }, + + async loadTasks() { + this.loading = true; + this.error = null; + + try { + let url = '/admin/background-tasks/tasks?limit=50'; + if (this.filterType) { + url += `&task_type=${this.filterType}`; + } + + const tasks = await apiClient.get(url); + this.tasks = tasks; + backgroundTasksLog.info('Tasks loaded:', tasks.length); + } catch (err) { + backgroundTasksLog.error('Failed to load tasks:', err); + this.error = err.message; + + if (err.message.includes('Unauthorized')) { + window.location.href = '/admin/login'; + } + } finally { + this.loading = false; + } + }, + + async loadRunningTasks() { + try { + const running = await apiClient.get('/admin/background-tasks/tasks/running'); + this.runningTasks = running; + + // Update elapsed time for running tasks + const now = new Date(); + this.runningTasks.forEach(task => { + if (task.started_at) { + const started = new Date(task.started_at); + task.duration_seconds = (now - started) / 1000; + } + }); + } catch (err) { + backgroundTasksLog.error('Failed to load running tasks:', err); + } + }, + + async refresh() { + await this.loadStats(); + await this.loadTasks(); + await this.loadRunningTasks(); + }, + + formatDuration(seconds) { + if (seconds === null || seconds === undefined) return 'N/A'; + if (seconds < 1) return `${Math.round(seconds * 1000)}ms`; + if (seconds < 60) return `${Math.round(seconds)}s`; + const minutes = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + return `${minutes}m ${secs}s`; + } + }; +} diff --git a/app/modules/marketplace/static/admin/js/imports.js b/app/modules/marketplace/static/admin/js/imports.js new file mode 100644 index 00000000..67a56d8a --- /dev/null +++ b/app/modules/marketplace/static/admin/js/imports.js @@ -0,0 +1,468 @@ +// noqa: js-006 - async init pattern is safe, loadData has try/catch +// static/admin/js/imports.js +/** + * Admin platform monitoring - all import jobs + */ + +// ✅ Use centralized logger +const adminImportsLog = window.LogConfig.loggers.imports; + +adminImportsLog.info('Loading...'); + +function adminImports() { + adminImportsLog.debug('adminImports() called'); + + return { + // ✅ Inherit base layout state + ...data(), + + // ✅ Set page identifier + currentPage: 'imports', + + // Loading states + loading: false, + error: '', + + // Vendors list + vendors: [], + + // Stats + stats: { + total: 0, + active: 0, + completed: 0, + failed: 0 + }, + + // Filters + filters: { + vendor_id: '', + status: '', + marketplace: '', + created_by: '' // 'me' or empty + }, + + // Import jobs + jobs: [], + pagination: { + page: 1, + per_page: 20, + total: 0, + pages: 0 + }, + + // Modal state + showJobModal: false, + selectedJob: null, + + // Job errors state + jobErrors: [], + jobErrorsTotal: 0, + jobErrorsPage: 1, + loadingErrors: false, + + // Auto-refresh for active jobs + autoRefreshInterval: null, + + // Computed: Total pages + get totalPages() { + return this.pagination.pages; + }, + + // Computed: Start index for pagination display + get startIndex() { + if (this.pagination.total === 0) return 0; + return (this.pagination.page - 1) * this.pagination.per_page + 1; + }, + + // Computed: End index for pagination display + get endIndex() { + const end = this.pagination.page * this.pagination.per_page; + return end > this.pagination.total ? this.pagination.total : end; + }, + + // Computed: Page numbers for pagination + get pageNumbers() { + const pages = []; + const totalPages = this.totalPages; + const current = this.pagination.page; + + if (totalPages <= 7) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + pages.push(1); + if (current > 3) { + pages.push('...'); + } + const start = Math.max(2, current - 1); + const end = Math.min(totalPages - 1, current + 1); + for (let i = start; i <= end; i++) { + pages.push(i); + } + if (current < totalPages - 2) { + pages.push('...'); + } + pages.push(totalPages); + } + return pages; + }, + + async init() { + // Guard against multiple initialization + if (window._adminImportsInitialized) { + return; + } + window._adminImportsInitialized = true; + + // Load platform settings for rows per page + if (window.PlatformSettings) { + this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); + } + + // IMPORTANT: Call parent init first + const parentInit = data().init; + if (parentInit) { + await parentInit.call(this); + } + + await this.loadVendors(); + await this.loadJobs(); + await this.loadStats(); + + // Auto-refresh active jobs every 15 seconds + this.startAutoRefresh(); + }, + + /** + * Load all vendors for filtering + */ + async loadVendors() { + try { + const response = await apiClient.get('/admin/vendors?limit=1000'); + this.vendors = response.vendors || []; + adminImportsLog.debug('Loaded vendors:', this.vendors.length); + } catch (error) { + adminImportsLog.error('Failed to load vendors:', error); + } + }, + + /** + * Load statistics + */ + async loadStats() { + try { + const response = await apiClient.get('/admin/marketplace-import-jobs/stats'); + this.stats = { + total: response.total || 0, + active: (response.pending || 0) + (response.processing || 0), + completed: response.completed || 0, + failed: response.failed || 0 + }; + adminImportsLog.debug('Loaded stats:', this.stats); + } catch (error) { + adminImportsLog.error('Failed to load stats:', error); + // Non-critical, don't show error + } + }, + + /** + * Load ALL import jobs (with filters) + */ + async loadJobs() { + this.loading = true; + this.error = ''; + + try { + // Build query params + const params = new URLSearchParams({ + skip: (this.pagination.page - 1) * this.pagination.per_page, + limit: this.pagination.per_page + }); + + // Add filters + if (this.filters.vendor_id) { + params.append('vendor_id', this.filters.vendor_id); + } + if (this.filters.status) { + params.append('status', this.filters.status); + } + if (this.filters.marketplace) { + params.append('marketplace', this.filters.marketplace); + } + if (this.filters.created_by === 'me') { + params.append('created_by_me', 'true'); + } + + const response = await apiClient.get( + `/admin/marketplace-import-jobs?${params.toString()}` + ); + + this.jobs = response.items || []; + this.pagination.total = response.total || 0; + this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page); + + adminImportsLog.debug('Loaded all jobs:', this.jobs.length); + } catch (error) { + adminImportsLog.error('Failed to load jobs:', error); + this.error = error.message || 'Failed to load import jobs'; + } finally { + this.loading = false; + } + }, + + /** + * Apply filters and reload + */ + async applyFilters() { + this.pagination.page = 1; // Reset to first page when filtering + await this.loadJobs(); + await this.loadStats(); // Update stats based on filters + }, + + /** + * Clear all filters and reload + */ + async clearFilters() { + this.filters.vendor_id = ''; + this.filters.status = ''; + this.filters.marketplace = ''; + this.filters.created_by = ''; + this.pagination.page = 1; + await this.loadJobs(); + await this.loadStats(); + }, + + /** + * Refresh jobs list + */ + async refreshJobs() { + await this.loadJobs(); + await this.loadStats(); + }, + + /** + * Refresh single job status + */ + async refreshJobStatus(jobId) { + try { + const response = await apiClient.get(`/admin/marketplace-import-jobs/${jobId}`); + + // Update job in list + const index = this.jobs.findIndex(j => j.id === jobId); + if (index !== -1) { + this.jobs[index] = response; + } + + // Update selected job if modal is open + if (this.selectedJob && this.selectedJob.id === jobId) { + this.selectedJob = response; + } + + adminImportsLog.debug('Refreshed job:', jobId); + } catch (error) { + adminImportsLog.error('Failed to refresh job:', error); + } + }, + + /** + * View job details in modal + */ + async viewJobDetails(jobId) { + try { + const response = await apiClient.get(`/admin/marketplace-import-jobs/${jobId}`); + this.selectedJob = response; + this.showJobModal = true; + adminImportsLog.debug('Viewing job details:', jobId); + } catch (error) { + adminImportsLog.error('Failed to load job details:', error); + this.error = error.message || 'Failed to load job details'; + } + }, + + /** + * Close job details modal + */ + closeJobModal() { + this.showJobModal = false; + this.selectedJob = null; + // Clear errors state + this.jobErrors = []; + this.jobErrorsTotal = 0; + this.jobErrorsPage = 1; + }, + + /** + * Load errors for a specific job + */ + async loadJobErrors(jobId) { + if (!jobId) return; + + this.loadingErrors = true; + this.jobErrorsPage = 1; + + try { + const response = await apiClient.get( + `/admin/marketplace-import-jobs/${jobId}/errors?page=1&limit=20` + ); + this.jobErrors = response.errors || []; + this.jobErrorsTotal = response.total || 0; + adminImportsLog.debug('Loaded job errors:', this.jobErrors.length); + } catch (error) { + adminImportsLog.error('Failed to load job errors:', error); + this.error = error.message || 'Failed to load import errors'; + } finally { + this.loadingErrors = false; + } + }, + + /** + * Load more errors (pagination) + */ + async loadMoreJobErrors(jobId) { + if (!jobId || this.loadingErrors) return; + + this.loadingErrors = true; + this.jobErrorsPage++; + + try { + const response = await apiClient.get( + `/admin/marketplace-import-jobs/${jobId}/errors?page=${this.jobErrorsPage}&limit=20` + ); + const newErrors = response.errors || []; + this.jobErrors = [...this.jobErrors, ...newErrors]; + adminImportsLog.debug('Loaded more job errors:', newErrors.length); + } catch (error) { + adminImportsLog.error('Failed to load more job errors:', error); + this.jobErrorsPage--; // Revert page on failure + } finally { + this.loadingErrors = false; + } + }, + + /** + * Get vendor name by ID + */ + getVendorName(vendorId) { + const vendor = this.vendors.find(v => v.id === vendorId); + return vendor ? `${vendor.name} (${vendor.vendor_code})` : `Vendor #${vendorId}`; + }, + + /** + * Pagination: Previous page + */ + previousPage() { + if (this.pagination.page > 1) { + this.pagination.page--; + this.loadJobs(); + } + }, + + /** + * Pagination: Next page + */ + nextPage() { + if (this.pagination.page < this.totalPages) { + this.pagination.page++; + this.loadJobs(); + } + }, + + /** + * Pagination: Go to specific page + */ + goToPage(pageNum) { + if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.totalPages) { + this.pagination.page = pageNum; + this.loadJobs(); + } + }, + + /** + * Format date for display + */ + formatDate(dateString) { + if (!dateString) return 'N/A'; + + try { + const date = new Date(dateString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + return dateString; + } + }, + + /** + * Calculate duration between start and end + */ + calculateDuration(job) { + if (!job.started_at) { + return 'Not started'; + } + + const start = new Date(job.started_at); + const end = job.completed_at ? new Date(job.completed_at) : new Date(); + const durationMs = end - start; + + // Convert to human-readable format + const seconds = Math.floor(durationMs / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else { + return `${seconds}s`; + } + }, + + /** + * Start auto-refresh for active jobs + */ + startAutoRefresh() { + // Clear any existing interval + if (this.autoRefreshInterval) { + clearInterval(this.autoRefreshInterval); + } + + // Refresh every 15 seconds if there are active jobs + this.autoRefreshInterval = setInterval(async () => { + const hasActiveJobs = this.jobs.some(job => + job.status === 'pending' || job.status === 'processing' + ); + + if (hasActiveJobs) { + adminImportsLog.debug('Auto-refreshing active jobs...'); + await this.loadJobs(); + await this.loadStats(); + } + }, 15000); // 15 seconds + }, + + /** + * Stop auto-refresh (cleanup) + */ + stopAutoRefresh() { + if (this.autoRefreshInterval) { + clearInterval(this.autoRefreshInterval); + this.autoRefreshInterval = null; + } + } + }; +} + +// Cleanup on page unload +window.addEventListener('beforeunload', () => { + if (window._adminImportsInstance && window._adminImportsInstance.stopAutoRefresh) { + window._adminImportsInstance.stopAutoRefresh(); + } +});