803 lines
27 KiB
Vue

<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-storage"></Icon>
<h1 class="text-xl font-bold text-primary">Queue Persistence Configuration</h1>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center">
<div class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
<span class="text-sm text-gray-600">Persistence Active</span>
</div>
<rs-button variant="outline" size="sm" @click="testPersistence">
<Icon class="mr-1" name="ic:outline-bug-report"></Icon>
Test Recovery
</rs-button>
</div>
</div>
</template>
<template #body>
<p class="text-gray-600">
Configure queue data persistence to ensure notifications survive system restarts and failures.
Critical for maintaining queue integrity and preventing message loss during system maintenance.
</p>
</template>
</rs-card>
<!-- Persistence Status Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(metric, index) in persistenceMetrics"
: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="metric.bgColor"
>
<Icon class="text-2xl" :class="metric.iconColor" :name="metric.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl leading-tight" :class="metric.valueColor">
{{ metric.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ metric.title }}
</span>
<div class="flex items-center mt-1" v-if="metric.status">
<div
class="w-2 h-2 rounded-full mr-1"
:class="{
'bg-green-500': metric.status === 'healthy',
'bg-yellow-500': metric.status === 'warning',
'bg-red-500': metric.status === 'error'
}"
></div>
<span class="text-xs capitalize" :class="{
'text-green-600': metric.status === 'healthy',
'text-yellow-600': metric.status === 'warning',
'text-red-600': metric.status === 'error'
}">
{{ metric.status }}
</span>
</div>
</div>
</div>
</rs-card>
</div>
<!-- Storage Configuration -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Primary Storage -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-database"></Icon>
<h3 class="text-lg font-semibold text-primary">Primary Storage Configuration</h3>
</div>
<rs-button variant="outline" size="sm" @click="showStorageModal = true">
<Icon class="mr-1" name="ic:outline-settings"></Icon>
Configure
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Storage Type -->
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="font-medium">Storage Type</p>
<p class="text-sm text-gray-600">{{ storageConfig.type }}</p>
</div>
<div class="text-right">
<span
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
:class="{
'bg-green-100 text-green-800': storageConfig.status === 'connected',
'bg-red-100 text-red-800': storageConfig.status === 'disconnected',
'bg-yellow-100 text-yellow-800': storageConfig.status === 'reconnecting'
}"
>
{{ storageConfig.status }}
</span>
</div>
</div>
<!-- Connection Details -->
<div class="grid grid-cols-2 gap-4">
<div class="text-center p-3 bg-blue-50 rounded-lg">
<p class="text-sm text-gray-600">Connection Pool</p>
<p class="font-bold text-blue-600">{{ storageConfig.connectionPool }}/{{ storageConfig.maxConnections }}</p>
</div>
<div class="text-center p-3 bg-green-50 rounded-lg">
<p class="text-sm text-gray-600">Response Time</p>
<p class="font-bold text-green-600">{{ storageConfig.responseTime }}ms</p>
</div>
</div>
<!-- Storage Metrics -->
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm text-gray-600">Used Space</span>
<span class="text-sm font-medium">{{ storageConfig.usedSpace }} / {{ storageConfig.totalSpace }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full"
:style="{ width: storageConfig.usagePercentage + '%' }"
></div>
</div>
</div>
<!-- Last Backup -->
<div class="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p class="font-medium text-green-800">Last Backup</p>
<p class="text-sm text-green-600">{{ storageConfig.lastBackup }}</p>
</div>
<Icon class="text-green-600" name="ic:outline-backup"></Icon>
</div>
</div>
</template>
</rs-card>
<!-- Backup & Recovery -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-backup"></Icon>
<h3 class="text-lg font-semibold text-primary">Backup & Recovery</h3>
</div>
<rs-button variant="outline" size="sm" @click="createBackup">
<Icon class="mr-1" name="ic:outline-backup"></Icon>
Create Backup
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Backup Schedule -->
<div class="p-3 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-2">
<p class="font-medium">Automatic Backups</p>
<div class="flex items-center">
<input
type="checkbox"
v-model="backupConfig.autoBackupEnabled"
class="mr-2"
@change="updateBackupConfig"
>
<span class="text-sm">{{ backupConfig.autoBackupEnabled ? 'Enabled' : 'Disabled' }}</span>
</div>
</div>
<p class="text-sm text-gray-600">
Frequency: {{ backupConfig.frequency }} |
Retention: {{ backupConfig.retention }} days
</p>
</div>
<!-- Recent Backups -->
<div>
<h4 class="font-medium text-gray-700 mb-2">Recent Backups</h4>
<div class="space-y-2">
<div
v-for="(backup, index) in recentBackups"
:key="index"
class="flex items-center justify-between p-2 bg-gray-50 rounded"
>
<div class="flex items-center">
<Icon
class="mr-2 text-sm"
:class="{
'text-green-500': backup.status === 'completed',
'text-yellow-500': backup.status === 'in-progress',
'text-red-500': backup.status === 'failed'
}"
:name="backup.status === 'completed' ? 'ic:outline-check-circle' :
backup.status === 'in-progress' ? 'ic:outline-hourglass-empty' :
'ic:outline-error'"
></Icon>
<div>
<p class="text-sm font-medium">{{ backup.name }}</p>
<p class="text-xs text-gray-500">{{ backup.size }} {{ backup.timestamp }}</p>
</div>
</div>
<div class="flex items-center gap-1">
<rs-button
variant="outline"
size="xs"
@click="downloadBackup(backup)"
:disabled="backup.status !== 'completed'"
>
<Icon class="text-xs" name="ic:outline-download"></Icon>
</rs-button>
<rs-button
variant="outline"
size="xs"
@click="restoreBackup(backup)"
:disabled="backup.status !== 'completed'"
>
<Icon class="text-xs" name="ic:outline-restore"></Icon>
</rs-button>
</div>
</div>
</div>
</div>
<!-- Recovery Test -->
<div class="p-3 bg-yellow-50 rounded-lg">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-yellow-800">Recovery Test</p>
<p class="text-sm text-yellow-600">Last test: {{ recoveryTest.lastTest }}</p>
</div>
<rs-button
variant="outline"
size="sm"
@click="runRecoveryTest"
>
Run Test
</rs-button>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Queue Recovery Status -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-restore"></Icon>
<h3 class="text-lg font-semibold text-primary">Queue Recovery Status</h3>
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-600">Last System Restart: {{ lastSystemRestart }}</span>
<rs-button variant="outline" size="sm" @click="showRecoveryDetails = true">
<Icon class="mr-1" name="ic:outline-info"></Icon>
View Details
</rs-button>
</div>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Recovery Statistics -->
<div class="space-y-4">
<h4 class="font-medium text-gray-700">Recovery Statistics</h4>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-sm text-gray-600">Jobs Recovered</span>
<span class="font-medium text-green-600">{{ recoveryStats.jobsRecovered.toLocaleString() }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Jobs Lost</span>
<span class="font-medium text-red-600">{{ recoveryStats.jobsLost }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Recovery Time</span>
<span class="font-medium">{{ recoveryStats.recoveryTime }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Success Rate</span>
<span class="font-medium text-blue-600">{{ recoveryStats.successRate }}%</span>
</div>
</div>
</div>
<!-- Recovery Timeline -->
<div class="space-y-4">
<h4 class="font-medium text-gray-700">Recovery Timeline</h4>
<div class="space-y-3">
<div
v-for="(event, index) in recoveryTimeline"
:key="index"
class="flex items-start"
>
<div
class="w-3 h-3 rounded-full mt-1 mr-3"
:class="{
'bg-green-500': event.status === 'completed',
'bg-yellow-500': event.status === 'in-progress',
'bg-red-500': event.status === 'failed'
}"
></div>
<div>
<p class="text-sm font-medium">{{ event.action }}</p>
<p class="text-xs text-gray-500">{{ event.timestamp }}</p>
</div>
</div>
</div>
</div>
<!-- Queue State -->
<div class="space-y-4">
<h4 class="font-medium text-gray-700">Current Queue State</h4>
<div class="space-y-3">
<div
v-for="(queue, index) in queueStates"
:key="index"
class="p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center justify-between mb-1">
<span class="text-sm font-medium">{{ queue.name }}</span>
<span
class="text-xs px-2 py-1 rounded-full"
:class="{
'bg-green-100 text-green-800': queue.status === 'healthy',
'bg-yellow-100 text-yellow-800': queue.status === 'recovering',
'bg-red-100 text-red-800': queue.status === 'error'
}"
>
{{ queue.status }}
</span>
</div>
<div class="flex justify-between text-xs text-gray-600">
<span>{{ queue.count }} jobs</span>
<span>{{ queue.lastProcessed }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Persistence Configuration -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-settings"></Icon>
<h3 class="text-lg font-semibold text-primary">Persistence Settings</h3>
</div>
<rs-button @click="savePersistenceConfig">
<Icon class="mr-1" name="ic:outline-save"></Icon>
Save Configuration
</rs-button>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- General Settings -->
<div class="space-y-4">
<h4 class="font-medium text-gray-700">General Settings</h4>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Persistence Mode</label>
<select v-model="persistenceConfig.mode" class="w-full p-2 border border-gray-300 rounded-md">
<option value="immediate">Immediate (Every job)</option>
<option value="batch">Batch (Every N jobs)</option>
<option value="interval">Interval (Every N seconds)</option>
<option value="hybrid">Hybrid (Immediate + Batch)</option>
</select>
</div>
<div v-if="persistenceConfig.mode === 'batch'">
<label class="block text-sm font-medium text-gray-700 mb-2">Batch Size</label>
<input
type="number"
v-model="persistenceConfig.batchSize"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
>
</div>
<div v-if="persistenceConfig.mode === 'interval'">
<label class="block text-sm font-medium text-gray-700 mb-2">Interval (seconds)</label>
<input
type="number"
v-model="persistenceConfig.interval"
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">Data Retention (days)</label>
<input
type="number"
v-model="persistenceConfig.retentionDays"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
>
<p class="text-xs text-gray-500 mt-1">How long to keep completed job data</p>
</div>
<div class="flex items-center">
<input
type="checkbox"
v-model="persistenceConfig.compressData"
class="mr-2"
>
<span class="text-sm text-gray-700">Enable data compression</span>
</div>
</div>
<!-- Recovery Settings -->
<div class="space-y-4">
<h4 class="font-medium text-gray-700">Recovery Settings</h4>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Recovery Strategy</label>
<select v-model="persistenceConfig.recoveryStrategy" class="w-full p-2 border border-gray-300 rounded-md">
<option value="full">Full Recovery (All jobs)</option>
<option value="priority">Priority Recovery (High priority first)</option>
<option value="recent">Recent Recovery (Last N hours)</option>
<option value="selective">Selective Recovery (Manual selection)</option>
</select>
</div>
<div v-if="persistenceConfig.recoveryStrategy === 'recent'">
<label class="block text-sm font-medium text-gray-700 mb-2">Recovery Window (hours)</label>
<input
type="number"
v-model="persistenceConfig.recoveryWindow"
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 Recovery Time (seconds)</label>
<input
type="number"
v-model="persistenceConfig.maxRecoveryTime"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
>
<p class="text-xs text-gray-500 mt-1">Maximum time allowed for recovery process</p>
</div>
<div class="flex items-center">
<input
type="checkbox"
v-model="persistenceConfig.autoRecovery"
class="mr-2"
>
<span class="text-sm text-gray-700">Enable automatic recovery on startup</span>
</div>
<div class="flex items-center">
<input
type="checkbox"
v-model="persistenceConfig.validateRecovery"
class="mr-2"
>
<span class="text-sm text-gray-700">Validate recovered jobs before processing</span>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Storage Configuration Modal -->
<rs-modal v-model="showStorageModal" title="Storage Configuration">
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Storage Type</label>
<select v-model="storageConfig.type" class="w-full p-2 border border-gray-300 rounded-md">
<option value="Redis">Redis</option>
<option value="PostgreSQL">PostgreSQL</option>
<option value="MongoDB">MongoDB</option>
<option value="MySQL">MySQL</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Connection String</label>
<input
type="text"
v-model="storageConfig.connectionString"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="redis://localhost:6379"
>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Max Connections</label>
<input
type="number"
v-model="storageConfig.maxConnections"
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">Connection Timeout (ms)</label>
<input
type="number"
v-model="storageConfig.connectionTimeout"
class="w-full p-2 border border-gray-300 rounded-md"
min="100"
>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3">
<rs-button variant="outline" @click="showStorageModal = false">
Cancel
</rs-button>
<rs-button @click="saveStorageConfig">
Save Configuration
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Queue Persistence",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Persistence",
path: "/notification/queue-scheduler/persistence",
},
],
});
// Reactive data
const showStorageModal = ref(false);
const showRecoveryDetails = ref(false);
// Persistence metrics
const persistenceMetrics = ref([
{
title: "Storage Health",
value: "Healthy",
icon: "ic:outline-health-and-safety",
bgColor: "bg-green-100",
iconColor: "text-green-600",
valueColor: "text-green-600",
status: "healthy"
},
{
title: "Persisted Jobs",
value: "847,293",
icon: "ic:outline-storage",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
valueColor: "text-blue-600"
},
{
title: "Recovery Rate",
value: "99.97%",
icon: "ic:outline-restore",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
valueColor: "text-purple-600",
status: "healthy"
},
{
title: "Storage Usage",
value: "67%",
icon: "ic:outline-pie-chart",
bgColor: "bg-yellow-100",
iconColor: "text-yellow-600",
valueColor: "text-yellow-600",
status: "warning"
}
]);
// Storage configuration
const storageConfig = ref({
type: "Redis",
status: "connected",
connectionPool: 8,
maxConnections: 20,
responseTime: 2.3,
usedSpace: "2.4 GB",
totalSpace: "10 GB",
usagePercentage: 67,
lastBackup: "2 hours ago",
connectionString: "redis://localhost:6379",
connectionTimeout: 5000
});
// Backup configuration
const backupConfig = ref({
autoBackupEnabled: true,
frequency: "Every 6 hours",
retention: 30
});
// Recent backups
const recentBackups = ref([
{
name: "queue-backup-2024-01-15-14-30",
size: "1.2 GB",
timestamp: "2 hours ago",
status: "completed"
},
{
name: "queue-backup-2024-01-15-08-30",
size: "1.1 GB",
timestamp: "8 hours ago",
status: "completed"
},
{
name: "queue-backup-2024-01-15-02-30",
size: "1.0 GB",
timestamp: "14 hours ago",
status: "completed"
},
{
name: "queue-backup-2024-01-14-20-30",
size: "987 MB",
timestamp: "20 hours ago",
status: "completed"
}
]);
// Recovery test
const recoveryTest = ref({
lastTest: "3 days ago",
status: "passed"
});
// System restart info
const lastSystemRestart = ref("5 days ago");
// Recovery statistics
const recoveryStats = ref({
jobsRecovered: 15847,
jobsLost: 3,
recoveryTime: "2.3 seconds",
successRate: 99.97
});
// Recovery timeline
const recoveryTimeline = ref([
{
action: "System startup detected",
timestamp: "5 days ago, 09:15:23",
status: "completed"
},
{
action: "Storage connection established",
timestamp: "5 days ago, 09:15:24",
status: "completed"
},
{
action: "Queue data recovery initiated",
timestamp: "5 days ago, 09:15:25",
status: "completed"
},
{
action: "15,847 jobs recovered successfully",
timestamp: "5 days ago, 09:15:27",
status: "completed"
},
{
action: "Queue processing resumed",
timestamp: "5 days ago, 09:15:28",
status: "completed"
}
]);
// Queue states
const queueStates = ref([
{
name: "High Priority",
count: 234,
status: "healthy",
lastProcessed: "2 seconds ago"
},
{
name: "Medium Priority",
count: 1847,
status: "healthy",
lastProcessed: "1 second ago"
},
{
name: "Low Priority",
count: 3421,
status: "healthy",
lastProcessed: "5 seconds ago"
},
{
name: "Bulk Operations",
count: 2502,
status: "recovering",
lastProcessed: "30 seconds ago"
}
]);
// Persistence configuration
const persistenceConfig = ref({
mode: "hybrid",
batchSize: 100,
interval: 30,
retentionDays: 30,
compressData: true,
recoveryStrategy: "priority",
recoveryWindow: 24,
maxRecoveryTime: 300,
autoRecovery: true,
validateRecovery: true
});
// Methods
const testPersistence = () => {
console.log('Running persistence test...');
// Simulate persistence test
};
const createBackup = () => {
console.log('Creating backup...');
// Add new backup to the list
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
recentBackups.value.unshift({
name: `queue-backup-${timestamp}`,
size: "1.3 GB",
timestamp: "Just now",
status: "in-progress"
});
// Simulate completion after 3 seconds
setTimeout(() => {
recentBackups.value[0].status = "completed";
}, 3000);
};
const downloadBackup = (backup) => {
console.log('Downloading backup:', backup.name);
// Simulate download
};
const restoreBackup = (backup) => {
console.log('Restoring backup:', backup.name);
// Simulate restore
};
const runRecoveryTest = () => {
console.log('Running recovery test...');
recoveryTest.value.lastTest = "Just now";
// Simulate test
};
const updateBackupConfig = () => {
console.log('Updating backup configuration:', backupConfig.value);
// Save backup config
};
const savePersistenceConfig = () => {
console.log('Saving persistence configuration:', persistenceConfig.value);
// Save persistence config
};
const saveStorageConfig = () => {
console.log('Saving storage configuration:', storageConfig.value);
showStorageModal.value = false;
// Save storage config
};
</script>
<style lang="scss" scoped></style>