- 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
152 lines
5.1 KiB
Vue
152 lines
5.1 KiB
Vue
<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>
|