feat(system): implémenter la page Logs des erreurs de scraping
- 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
This commit is contained in:
parent
36f873aaca
commit
670e3f5315
151
assets/vue/app/domain/system/presentation/pages/LogsPage.vue
Normal file
151
assets/vue/app/domain/system/presentation/pages/LogsPage.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<Toolbar :config="toolbarConfig" />
|
||||
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<section class="border-t border-gray-200 dark:border-gray-700">
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="flex justify-center py-12">
|
||||
<div class="animate-spin h-10 w-10 border-b-2 border-blue-500 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-else-if="hasError" class="px-6 py-8">
|
||||
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 p-4">
|
||||
<div class="flex items-center">
|
||||
<ExclamationCircleIcon class="w-5 h-5 text-red-400 mr-2 shrink-0" />
|
||||
<p class="text-red-800 dark:text-red-200">{{ error }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="logsStore.loadLogs()"
|
||||
class="mt-3 px-4 py-2 bg-red-600 text-white hover:bg-red-700">
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="!isLoading && logs.length === 0" class="flex flex-col items-center justify-center py-20 text-gray-400 dark:text-gray-500">
|
||||
<ExclamationCircleIcon class="w-12 h-12 mb-3" />
|
||||
<p class="text-base">Aucune erreur de scraping</p>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<template v-else>
|
||||
<LogItem
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
:log="log"
|
||||
:source="getSource(log)"
|
||||
@delete="handleDelete" />
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination
|
||||
v-if="totalPages > 1"
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
:total="total"
|
||||
:limit="limit"
|
||||
:has-next-page="hasNextPage"
|
||||
:has-previous-page="hasPreviousPage"
|
||||
@page-change="logsStore.goToPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowPathIcon, ExclamationCircleIcon, TrashIcon } from '@heroicons/vue/24/outline';
|
||||
import { BarsArrowDownIcon } from '@heroicons/vue/24/outline';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import Pagination from '../../../../shared/components/ui/Pagination.vue';
|
||||
import { useContentSourceStore } from '../../../setting/application/store/contentSourceStore';
|
||||
import { useLogsStore } from '../../application/store/logsStore';
|
||||
import LogItem from '../components/LogItem.vue';
|
||||
|
||||
const logsStore = useLogsStore();
|
||||
const contentSourceStore = useContentSourceStore();
|
||||
const { sources } = storeToRefs(contentSourceStore);
|
||||
|
||||
const {
|
||||
logs,
|
||||
loading: isLoading,
|
||||
error,
|
||||
currentPage,
|
||||
totalPages,
|
||||
total,
|
||||
limit,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
} = storeToRefs(logsStore);
|
||||
|
||||
const hasError = computed(() => !!error.value);
|
||||
|
||||
onMounted(() => {
|
||||
logsStore.loadLogs();
|
||||
contentSourceStore.loadSources();
|
||||
});
|
||||
|
||||
function getSource(log) {
|
||||
const sourceId = log.context?.sourceId;
|
||||
if (!sourceId) return null;
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return sources.value.find(s => s.id == sourceId) ?? null;
|
||||
}
|
||||
|
||||
const isSortSelected = (by, order) => sortBy.value === by && sortOrder.value === order;
|
||||
|
||||
const toolbarConfig = computed(() => ({
|
||||
leftSection: [
|
||||
{ type: 'label', text: 'Logs', class: 'text-sm font-medium' },
|
||||
{ type: 'label', text: `(${total.value})`, class: 'text-sm text-gray-400' },
|
||||
],
|
||||
rightSection: [
|
||||
{
|
||||
type: 'dropdown',
|
||||
icon: BarsArrowDownIcon,
|
||||
label: 'Trier',
|
||||
items: [
|
||||
{
|
||||
label: 'Plus récent',
|
||||
isSelected: isSortSelected('createdAt', 'DESC'),
|
||||
onClick: () => logsStore.updateSort('createdAt', 'DESC'),
|
||||
},
|
||||
{
|
||||
label: 'Plus ancien',
|
||||
isSelected: isSortSelected('createdAt', 'ASC'),
|
||||
onClick: () => logsStore.updateSort('createdAt', 'ASC'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: ArrowPathIcon,
|
||||
label: 'Rafraîchir',
|
||||
disabled: isLoading.value,
|
||||
onClick: () => logsStore.loadLogs(),
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
icon: TrashIcon,
|
||||
label: 'Tout supprimer',
|
||||
disabled: isLoading.value || total.value === 0,
|
||||
onClick: handleDeleteAll,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
async function handleDelete(id) {
|
||||
await logsStore.deleteLog(id);
|
||||
}
|
||||
|
||||
async function handleDeleteAll() {
|
||||
if (!confirm('Supprimer tous les logs d\'erreur ? Cette action est irréversible.')) return;
|
||||
await logsStore.deleteAllLogs();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user