From 670e3f5315cbac951a5ed125a49f915fcb86b51a Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Mon, 16 Mar 2026 14:43:19 +0100 Subject: [PATCH] =?UTF-8?q?feat(system):=20impl=C3=A9menter=20la=20page=20?= =?UTF-8?q?Logs=20des=20erreurs=20de=20scraping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau domaine `system` avec `logsStore` (Pinia) filtré sur status=failed&type=scraping_job, tri, pagination et suppression - Composant `LogItem` : affiche titre manga, chapitre, date, durée, domaine source (lien vers page d'édition), badge type scraping, slug utilisé, message d'erreur expandable - Page `LogsPage` : toolbar avec badge total, dropdown tri, rafraîchir, tout supprimer ; charge les ContentSources pour enrichir l'affichage - Route /system/logs branchée sur LogsPage - ApiJobRepository : ajout du paramètre `type` dans getJobs - Job entity : ajout des champs startedAt et completedAt --- .../domain/activity/domain/entities/job.js | 4 + .../infrastructure/api/ApiJobRepository.js | 7 +- .../system/application/store/logsStore.js | 93 +++++++++++ .../presentation/components/LogItem.vue | 122 ++++++++++++++ .../system/presentation/pages/LogsPage.vue | 151 ++++++++++++++++++ assets/vue/app/router/index.js | 4 +- 6 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 assets/vue/app/domain/system/application/store/logsStore.js create mode 100644 assets/vue/app/domain/system/presentation/components/LogItem.vue create mode 100644 assets/vue/app/domain/system/presentation/pages/LogsPage.vue diff --git a/assets/vue/app/domain/activity/domain/entities/job.js b/assets/vue/app/domain/activity/domain/entities/job.js index ea1882c..0898540 100644 --- a/assets/vue/app/domain/activity/domain/entities/job.js +++ b/assets/vue/app/domain/activity/domain/entities/job.js @@ -10,6 +10,8 @@ export class Job { failureReason = null, createdAt = new Date().toISOString(), updatedAt = new Date().toISOString(), + startedAt = null, + completedAt = null, attempts = 0, maxAttempts = 1, context = {} @@ -23,6 +25,8 @@ export class Job { this.error = failureReason ?? error; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.startedAt = startedAt; + this.completedAt = completedAt; this.attempts = attempts; this.maxAttempts = maxAttempts; this.context = context; diff --git a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js index 427b374..eca2524 100644 --- a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js +++ b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js @@ -13,7 +13,7 @@ export class ApiJobRepository extends JobRepositoryInterface { * @returns {Promise} Collection de jobs */ async getJobs(options = {}) { - const { page = 1, limit = 100, sortBy = 'createdAt', sortOrder = 'DESC', status = [] } = options; + const { page = 1, limit = 100, sortBy = 'createdAt', sortOrder = 'DESC', status = [], type = null } = options; try { let url = `/api/jobs?page=${page}&limit=${limit}&sortBy=${sortBy}&sortOrder=${sortOrder}`; @@ -23,6 +23,11 @@ export class ApiJobRepository extends JobRepositoryInterface { url += `&status=${status.join(',')}`; } + // Ajouter le filtre de type si fourni + if (type) { + url += `&type=${type}`; + } + const response = await fetch(url); if (!response.ok) { diff --git a/assets/vue/app/domain/system/application/store/logsStore.js b/assets/vue/app/domain/system/application/store/logsStore.js new file mode 100644 index 0000000..d9ea76a --- /dev/null +++ b/assets/vue/app/domain/system/application/store/logsStore.js @@ -0,0 +1,93 @@ +import { defineStore } from 'pinia'; +import { ApiJobRepository } from '../../../activity/infrastructure/api/ApiJobRepository'; + +const jobRepository = new ApiJobRepository(); + +export const useLogsStore = defineStore('logs', { + state: () => ({ + logs: [], + loading: false, + error: null, + currentPage: 1, + totalPages: 0, + total: 0, + limit: 50, + hasNextPage: false, + hasPreviousPage: false, + sortBy: 'createdAt', + sortOrder: 'DESC', + }), + + getters: { + isLoading: state => state.loading, + hasError: state => !!state.error, + }, + + actions: { + async loadLogs(page = null) { + this.loading = true; + this.error = null; + + try { + const collection = await jobRepository.getJobs({ + page: page || this.currentPage, + limit: this.limit, + sortBy: this.sortBy, + sortOrder: this.sortOrder, + status: ['failed'], + type: 'scraping_job', + }); + + this.logs = collection.items; + this.currentPage = collection.page; + this.total = collection.total; + this.hasNextPage = collection.hasNextPage; + this.hasPreviousPage = collection.hasPreviousPage; + this.totalPages = Math.ceil(this.total / this.limit); + } catch (error) { + this.error = error.message; + } finally { + this.loading = false; + } + }, + + async goToPage(page) { + if (page >= 1 && page <= this.totalPages && page !== this.currentPage) { + this.currentPage = page; + await this.loadLogs(page); + } + }, + + async updateSort(sortBy, sortOrder) { + this.sortBy = sortBy; + this.sortOrder = sortOrder; + this.currentPage = 1; + await this.loadLogs(1); + }, + + async deleteLog(id) { + try { + await jobRepository.deleteJob(id); + this.logs = this.logs.filter(log => log.id !== id); + this.total = Math.max(0, this.total - 1); + this.totalPages = Math.ceil(this.total / this.limit); + } catch (error) { + this.error = error.message; + } + }, + + async deleteAllLogs() { + this.loading = true; + this.error = null; + + try { + await jobRepository.deleteJobs({ status: 'failed', type: 'scraping_job' }); + await this.loadLogs(1); + } catch (error) { + this.error = error.message; + } finally { + this.loading = false; + } + }, + }, +}); diff --git a/assets/vue/app/domain/system/presentation/components/LogItem.vue b/assets/vue/app/domain/system/presentation/components/LogItem.vue new file mode 100644 index 0000000..cd4be63 --- /dev/null +++ b/assets/vue/app/domain/system/presentation/components/LogItem.vue @@ -0,0 +1,122 @@ + + + diff --git a/assets/vue/app/domain/system/presentation/pages/LogsPage.vue b/assets/vue/app/domain/system/presentation/pages/LogsPage.vue new file mode 100644 index 0000000..c0f7999 --- /dev/null +++ b/assets/vue/app/domain/system/presentation/pages/LogsPage.vue @@ -0,0 +1,151 @@ + + + diff --git a/assets/vue/app/router/index.js b/assets/vue/app/router/index.js index ae30316..89f1b3a 100644 --- a/assets/vue/app/router/index.js +++ b/assets/vue/app/router/index.js @@ -10,6 +10,7 @@ import ChapterPage from '../domain/reader/presentation/pages/ChapterPage.vue'; import ScrapperConfigurations from '../domain/setting/presentation/pages/ScrapperConfigurations.vue'; import ScrapperEdit from '../domain/setting/presentation/pages/ScrapperEdit.vue'; import UserPreferencesPage from '../domain/setting/presentation/pages/UserPreferencesPage.vue'; +import LogsPage from '../domain/system/presentation/pages/LogsPage.vue'; import Layout from '../shared/components/layout/Layout.vue'; // Placeholder component for new routes @@ -148,8 +149,7 @@ const routes = [ { path: '/system/logs', name: 'system-logs', - component: PlaceholderComponent, - props: { title: 'Journaux système' } + component: LogsPage, }, { path: '/system/updates',