692 lines
23 KiB
Vue
692 lines
23 KiB
Vue
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Header Section -->
|
|
<rs-card class="mb-6">
|
|
<template #header>
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-refresh"></Icon>
|
|
<h1 class="text-xl font-bold text-primary">Retry & Dead Letter Queue</h1>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="text-gray-600">
|
|
Handles failed jobs with retry logic and dead letter queue for permanent failures.
|
|
Critical for debugging and ensuring no message is lost without explanation.
|
|
</p>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Statistics -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
|
<rs-card
|
|
v-for="(stat, index) in retryStats"
|
|
:key="index"
|
|
class="transition-all duration-300 hover:shadow-lg"
|
|
>
|
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
|
<div
|
|
class="p-4 flex justify-center items-center rounded-2xl"
|
|
:class="stat.bgColor"
|
|
>
|
|
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
|
|
</div>
|
|
<div class="flex-1 truncate">
|
|
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
|
|
{{ stat.value }}
|
|
</span>
|
|
<span class="text-sm font-medium text-gray-600">
|
|
{{ stat.title }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Retry Configuration -->
|
|
<rs-card class="mb-6">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-primary">Retry Configuration</h3>
|
|
<rs-button variant="outline" size="sm" @click="showRetryConfigModal = true">
|
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
|
Configure
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<div
|
|
v-for="(config, index) in retryConfigs"
|
|
:key="index"
|
|
class="border border-gray-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex items-center mb-3">
|
|
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
|
|
<h4 class="font-semibold">{{ config.type }}</h4>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600">Max Attempts:</span>
|
|
<span class="font-medium">{{ config.maxAttempts }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600">Backoff Strategy:</span>
|
|
<span class="font-medium">{{ config.backoffStrategy }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600">Initial Delay:</span>
|
|
<span class="font-medium">{{ config.initialDelay }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600">Max Delay:</span>
|
|
<span class="font-medium">{{ config.maxDelay }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Tabs for Failed Jobs and Dead Letter Queue -->
|
|
<rs-tab v-model="activeTab" class="mb-6">
|
|
<rs-tab-item name="failed" label="Failed Jobs">
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-primary">Failed Jobs</h3>
|
|
<div class="flex items-center gap-2">
|
|
<rs-button variant="outline" size="sm" @click="retryAllFailed">
|
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
|
Retry All
|
|
</rs-button>
|
|
<rs-button variant="outline" size="sm" @click="refreshFailedJobs">
|
|
<Icon class="mr-1" name="ic:outline-sync"></Icon>
|
|
Refresh
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<!-- Filters -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
|
|
<select v-model="failedFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
|
|
<option value="">All Types</option>
|
|
<option value="email">Email</option>
|
|
<option value="sms">SMS</option>
|
|
<option value="push">Push Notification</option>
|
|
<option value="webhook">Webhook</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Error Type</label>
|
|
<select v-model="failedFilters.errorType" class="w-full p-2 border border-gray-300 rounded-md">
|
|
<option value="">All Errors</option>
|
|
<option value="network">Network Error</option>
|
|
<option value="rate_limit">Rate Limit</option>
|
|
<option value="invalid_token">Invalid Token</option>
|
|
<option value="timeout">Timeout</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-end">
|
|
<rs-button @click="clearFailedFilters" variant="outline" class="w-full">
|
|
<Icon class="mr-1" name="ic:outline-clear"></Icon>
|
|
Clear Filters
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
|
|
<rs-table
|
|
:field="failedJobsFields"
|
|
:data="filteredFailedJobs"
|
|
:options="{ striped: true, hover: true }"
|
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
|
advanced
|
|
/>
|
|
</template>
|
|
</rs-card>
|
|
</rs-tab-item>
|
|
|
|
<rs-tab-item name="dead-letter" label="Dead Letter Queue">
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-primary">Dead Letter Queue</h3>
|
|
<div class="flex items-center gap-2">
|
|
<rs-button variant="outline" size="sm" @click="exportDeadLetters">
|
|
<Icon class="mr-1" name="ic:outline-download"></Icon>
|
|
Export
|
|
</rs-button>
|
|
<rs-button variant="outline" size="sm" @click="refreshDeadLetters">
|
|
<Icon class="mr-1" name="ic:outline-sync"></Icon>
|
|
Refresh
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<!-- Filters -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
|
|
<select v-model="deadLetterFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
|
|
<option value="">All Types</option>
|
|
<option value="email">Email</option>
|
|
<option value="sms">SMS</option>
|
|
<option value="push">Push Notification</option>
|
|
<option value="webhook">Webhook</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Date Range</label>
|
|
<select v-model="deadLetterFilters.dateRange" class="w-full p-2 border border-gray-300 rounded-md">
|
|
<option value="">All Time</option>
|
|
<option value="today">Today</option>
|
|
<option value="week">This Week</option>
|
|
<option value="month">This Month</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-end">
|
|
<rs-button @click="clearDeadLetterFilters" variant="outline" class="w-full">
|
|
<Icon class="mr-1" name="ic:outline-clear"></Icon>
|
|
Clear Filters
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
|
|
<rs-table
|
|
:field="deadLetterFields"
|
|
:data="filteredDeadLetters"
|
|
:options="{ striped: true, hover: true }"
|
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
|
advanced
|
|
/>
|
|
</template>
|
|
</rs-card>
|
|
</rs-tab-item>
|
|
</rs-tab>
|
|
|
|
<!-- Job Details Modal -->
|
|
<rs-modal v-model="showJobModal" size="lg">
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold">Job Details</h3>
|
|
</template>
|
|
<template #body>
|
|
<div v-if="selectedJob" class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Job ID</label>
|
|
<p class="text-sm text-gray-900">{{ selectedJob.id }}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Type</label>
|
|
<p class="text-sm text-gray-900">{{ selectedJob.type }}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Attempts</label>
|
|
<p class="text-sm text-gray-900">{{ selectedJob.attempts }}/{{ selectedJob.maxAttempts }}</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700">Last Attempt</label>
|
|
<p class="text-sm text-gray-900">{{ selectedJob.lastAttempt }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error History -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Error History</label>
|
|
<div class="space-y-2 max-h-40 overflow-y-auto">
|
|
<div
|
|
v-for="(error, index) in selectedJob.errorHistory"
|
|
:key="index"
|
|
class="bg-red-50 border border-red-200 rounded p-3"
|
|
>
|
|
<div class="flex justify-between items-start mb-1">
|
|
<span class="text-sm font-medium text-red-800">Attempt {{ error.attempt }}</span>
|
|
<span class="text-xs text-red-600">{{ error.timestamp }}</span>
|
|
</div>
|
|
<p class="text-sm text-red-700">{{ error.message }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payload -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Payload</label>
|
|
<pre class="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-40">{{ JSON.stringify(selectedJob.payload, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<rs-button variant="outline" @click="showJobModal = false">Close</rs-button>
|
|
<rs-button v-if="selectedJob?.canRetry" @click="retryJob(selectedJob)" variant="primary">
|
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
|
Retry Job
|
|
</rs-button>
|
|
<rs-button v-if="!selectedJob?.isDead" @click="moveToDeadLetter(selectedJob)" variant="danger">
|
|
<Icon class="mr-1" name="ic:outline-delete"></Icon>
|
|
Move to DLQ
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
|
|
<!-- Retry Configuration Modal -->
|
|
<rs-modal v-model="showRetryConfigModal" size="lg">
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold">Retry Configuration</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<div
|
|
v-for="(config, index) in retryConfigs"
|
|
:key="index"
|
|
class="border border-gray-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex items-center mb-4">
|
|
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
|
|
<h4 class="font-semibold">{{ config.type }} Configuration</h4>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Max Attempts</label>
|
|
<input
|
|
v-model.number="config.maxAttempts"
|
|
type="number"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
max="10"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Backoff Strategy</label>
|
|
<select v-model="config.backoffStrategy" class="w-full p-2 border border-gray-300 rounded-md">
|
|
<option value="fixed">Fixed</option>
|
|
<option value="exponential">Exponential</option>
|
|
<option value="linear">Linear</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Initial Delay (seconds)</label>
|
|
<input
|
|
v-model.number="config.initialDelay"
|
|
type="number"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Max Delay (seconds)</label>
|
|
<input
|
|
v-model.number="config.maxDelay"
|
|
type="number"
|
|
class="w-full p-2 border border-gray-300 rounded-md"
|
|
min="1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<rs-button variant="outline" @click="showRetryConfigModal = false">Cancel</rs-button>
|
|
<rs-button @click="saveRetryConfig" variant="primary">Save Configuration</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({
|
|
title: "Retry & Dead Letter Queue",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "Notification",
|
|
path: "/notification",
|
|
},
|
|
{
|
|
name: "Queue & Scheduler",
|
|
path: "/notification/queue-scheduler",
|
|
},
|
|
{
|
|
name: "Retry & Dead Letter",
|
|
path: "/notification/queue-scheduler/retry",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Reactive data
|
|
const activeTab = ref('failed');
|
|
const showJobModal = ref(false);
|
|
const showRetryConfigModal = ref(false);
|
|
const selectedJob = ref(null);
|
|
|
|
// Filters
|
|
const failedFilters = ref({
|
|
type: '',
|
|
errorType: ''
|
|
});
|
|
|
|
const deadLetterFilters = ref({
|
|
type: '',
|
|
dateRange: ''
|
|
});
|
|
|
|
// Statistics
|
|
const retryStats = ref([
|
|
{
|
|
title: "Failed Jobs",
|
|
value: "156",
|
|
icon: "ic:outline-error",
|
|
bgColor: "bg-red-100",
|
|
iconColor: "text-red-600",
|
|
textColor: "text-red-600"
|
|
},
|
|
{
|
|
title: "Retrying",
|
|
value: "23",
|
|
icon: "ic:outline-refresh",
|
|
bgColor: "bg-yellow-100",
|
|
iconColor: "text-yellow-600",
|
|
textColor: "text-yellow-600"
|
|
},
|
|
{
|
|
title: "Dead Letters",
|
|
value: "45",
|
|
icon: "ic:outline-delete",
|
|
bgColor: "bg-gray-100",
|
|
iconColor: "text-gray-600",
|
|
textColor: "text-gray-600"
|
|
},
|
|
{
|
|
title: "Success Rate",
|
|
value: "94.2%",
|
|
icon: "ic:outline-trending-up",
|
|
bgColor: "bg-green-100",
|
|
iconColor: "text-green-600",
|
|
textColor: "text-green-600"
|
|
}
|
|
]);
|
|
|
|
// Retry configurations
|
|
const retryConfigs = ref([
|
|
{
|
|
type: 'Email',
|
|
icon: 'ic:outline-email',
|
|
maxAttempts: 3,
|
|
backoffStrategy: 'exponential',
|
|
initialDelay: 30,
|
|
maxDelay: 300
|
|
},
|
|
{
|
|
type: 'SMS',
|
|
icon: 'ic:outline-sms',
|
|
maxAttempts: 5,
|
|
backoffStrategy: 'linear',
|
|
initialDelay: 10,
|
|
maxDelay: 120
|
|
},
|
|
{
|
|
type: 'Push',
|
|
icon: 'ic:outline-notifications',
|
|
maxAttempts: 2,
|
|
backoffStrategy: 'fixed',
|
|
initialDelay: 60,
|
|
maxDelay: 60
|
|
},
|
|
{
|
|
type: 'Webhook',
|
|
icon: 'ic:outline-webhook',
|
|
maxAttempts: 4,
|
|
backoffStrategy: 'exponential',
|
|
initialDelay: 15,
|
|
maxDelay: 600
|
|
}
|
|
]);
|
|
|
|
// Table fields for failed jobs
|
|
const failedJobsFields = ref([
|
|
{ key: 'id', label: 'Job ID', sortable: true },
|
|
{ key: 'type', label: 'Type', sortable: true },
|
|
{ key: 'attempts', label: 'Attempts', sortable: true },
|
|
{ key: 'lastError', label: 'Last Error', sortable: false },
|
|
{ key: 'lastAttempt', label: 'Last Attempt', sortable: true },
|
|
{ key: 'nextRetry', label: 'Next Retry', sortable: true },
|
|
{ key: 'actions', label: 'Actions', sortable: false }
|
|
]);
|
|
|
|
// Table fields for dead letter queue
|
|
const deadLetterFields = ref([
|
|
{ key: 'id', label: 'Job ID', sortable: true },
|
|
{ key: 'type', label: 'Type', sortable: true },
|
|
{ key: 'totalAttempts', label: 'Total Attempts', sortable: true },
|
|
{ key: 'finalError', label: 'Final Error', sortable: false },
|
|
{ key: 'failedAt', label: 'Failed At', sortable: true },
|
|
{ key: 'actions', label: 'Actions', sortable: false }
|
|
]);
|
|
|
|
// Mock failed jobs data
|
|
const failedJobs = ref([
|
|
{
|
|
id: 'job_fail_001',
|
|
type: 'email',
|
|
attempts: 2,
|
|
maxAttempts: 3,
|
|
lastError: 'SMTP connection timeout',
|
|
lastAttempt: '2024-01-15 10:30:00',
|
|
nextRetry: '2024-01-15 11:00:00',
|
|
canRetry: true,
|
|
isDead: false,
|
|
payload: { to: 'user@example.com', subject: 'Welcome!' },
|
|
errorHistory: [
|
|
{ attempt: 1, message: 'SMTP server unavailable', timestamp: '2024-01-15 10:00:00' },
|
|
{ attempt: 2, message: 'SMTP connection timeout', timestamp: '2024-01-15 10:30:00' }
|
|
]
|
|
},
|
|
{
|
|
id: 'job_fail_002',
|
|
type: 'sms',
|
|
attempts: 3,
|
|
maxAttempts: 5,
|
|
lastError: 'Invalid phone number format',
|
|
lastAttempt: '2024-01-15 10:25:00',
|
|
nextRetry: '2024-01-15 10:45:00',
|
|
canRetry: true,
|
|
isDead: false,
|
|
payload: { to: '+invalid', message: 'Your OTP is 123456' },
|
|
errorHistory: [
|
|
{ attempt: 1, message: 'Invalid phone number format', timestamp: '2024-01-15 10:00:00' },
|
|
{ attempt: 2, message: 'Invalid phone number format', timestamp: '2024-01-15 10:15:00' },
|
|
{ attempt: 3, message: 'Invalid phone number format', timestamp: '2024-01-15 10:25:00' }
|
|
]
|
|
}
|
|
]);
|
|
|
|
// Mock dead letter queue data
|
|
const deadLetters = ref([
|
|
{
|
|
id: 'job_dead_001',
|
|
type: 'push',
|
|
totalAttempts: 2,
|
|
finalError: 'Invalid device token - token expired',
|
|
failedAt: '2024-01-14 15:30:00',
|
|
canRetry: false,
|
|
isDead: true,
|
|
payload: { token: 'expired_token_123', title: 'New Message' },
|
|
errorHistory: [
|
|
{ attempt: 1, message: 'Invalid device token', timestamp: '2024-01-14 15:00:00' },
|
|
{ attempt: 2, message: 'Invalid device token - token expired', timestamp: '2024-01-14 15:30:00' }
|
|
]
|
|
},
|
|
{
|
|
id: 'job_dead_002',
|
|
type: 'webhook',
|
|
totalAttempts: 4,
|
|
finalError: 'Endpoint not found (404)',
|
|
failedAt: '2024-01-13 12:45:00',
|
|
canRetry: false,
|
|
isDead: true,
|
|
payload: { url: 'https://api.example.com/webhook', data: { event: 'order.completed' } },
|
|
errorHistory: [
|
|
{ attempt: 1, message: 'Connection timeout', timestamp: '2024-01-13 12:00:00' },
|
|
{ attempt: 2, message: 'Connection timeout', timestamp: '2024-01-13 12:15:00' },
|
|
{ attempt: 3, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:30:00' },
|
|
{ attempt: 4, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:45:00' }
|
|
]
|
|
}
|
|
]);
|
|
|
|
// Computed filtered data
|
|
const filteredFailedJobs = computed(() => {
|
|
let filtered = failedJobs.value;
|
|
|
|
if (failedFilters.value.type) {
|
|
filtered = filtered.filter(job => job.type === failedFilters.value.type);
|
|
}
|
|
|
|
// Add actions column
|
|
return filtered.map(job => ({
|
|
...job,
|
|
attempts: `${job.attempts}/${job.maxAttempts}`,
|
|
actions: h('div', { class: 'flex gap-2' }, [
|
|
h('button', {
|
|
class: 'text-blue-600 hover:text-blue-800 text-sm',
|
|
onClick: () => viewJobDetails(job)
|
|
}, 'View'),
|
|
h('button', {
|
|
class: 'text-green-600 hover:text-green-800 text-sm',
|
|
onClick: () => retryJob(job)
|
|
}, 'Retry'),
|
|
h('button', {
|
|
class: 'text-red-600 hover:text-red-800 text-sm',
|
|
onClick: () => moveToDeadLetter(job)
|
|
}, 'Move to DLQ')
|
|
])
|
|
}));
|
|
});
|
|
|
|
const filteredDeadLetters = computed(() => {
|
|
let filtered = deadLetters.value;
|
|
|
|
if (deadLetterFilters.value.type) {
|
|
filtered = filtered.filter(job => job.type === deadLetterFilters.value.type);
|
|
}
|
|
|
|
// Add actions column
|
|
return filtered.map(job => ({
|
|
...job,
|
|
actions: h('div', { class: 'flex gap-2' }, [
|
|
h('button', {
|
|
class: 'text-blue-600 hover:text-blue-800 text-sm',
|
|
onClick: () => viewJobDetails(job)
|
|
}, 'View'),
|
|
h('button', {
|
|
class: 'text-red-600 hover:text-red-800 text-sm',
|
|
onClick: () => deleteDeadLetter(job)
|
|
}, 'Delete')
|
|
])
|
|
}));
|
|
});
|
|
|
|
// Methods
|
|
function viewJobDetails(job) {
|
|
selectedJob.value = job;
|
|
showJobModal.value = true;
|
|
}
|
|
|
|
function retryJob(job) {
|
|
// Mock retry logic
|
|
job.attempts += 1;
|
|
job.lastAttempt = new Date().toLocaleString();
|
|
job.nextRetry = new Date(Date.now() + 30 * 60000).toLocaleString(); // 30 minutes from now
|
|
|
|
if (job.attempts >= job.maxAttempts) {
|
|
moveToDeadLetter(job);
|
|
}
|
|
|
|
showJobModal.value = false;
|
|
}
|
|
|
|
function retryAllFailed() {
|
|
failedJobs.value.forEach(job => {
|
|
if (job.canRetry) {
|
|
retryJob(job);
|
|
}
|
|
});
|
|
}
|
|
|
|
function moveToDeadLetter(job) {
|
|
// Move job to dead letter queue
|
|
const deadJob = {
|
|
...job,
|
|
totalAttempts: job.attempts,
|
|
finalError: job.lastError,
|
|
failedAt: new Date().toLocaleString(),
|
|
canRetry: false,
|
|
isDead: true
|
|
};
|
|
|
|
deadLetters.value.unshift(deadJob);
|
|
|
|
// Remove from failed jobs
|
|
const index = failedJobs.value.findIndex(j => j.id === job.id);
|
|
if (index > -1) {
|
|
failedJobs.value.splice(index, 1);
|
|
}
|
|
|
|
showJobModal.value = false;
|
|
}
|
|
|
|
function deleteDeadLetter(job) {
|
|
const index = deadLetters.value.findIndex(j => j.id === job.id);
|
|
if (index > -1) {
|
|
deadLetters.value.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
function clearFailedFilters() {
|
|
failedFilters.value = {
|
|
type: '',
|
|
errorType: ''
|
|
};
|
|
}
|
|
|
|
function clearDeadLetterFilters() {
|
|
deadLetterFilters.value = {
|
|
type: '',
|
|
dateRange: ''
|
|
};
|
|
}
|
|
|
|
function refreshFailedJobs() {
|
|
// Mock refresh
|
|
console.log('Refreshing failed jobs...');
|
|
}
|
|
|
|
function refreshDeadLetters() {
|
|
// Mock refresh
|
|
console.log('Refreshing dead letters...');
|
|
}
|
|
|
|
function exportDeadLetters() {
|
|
// Mock export
|
|
console.log('Exporting dead letters...');
|
|
}
|
|
|
|
function saveRetryConfig() {
|
|
// Mock save
|
|
console.log('Saving retry configuration...');
|
|
showRetryConfigModal.value = false;
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped></style> |