Add notification log audit and analytics pages, implement real-time monitoring, performance tracking, and queue scheduler features. Create components for reports, priority management, and queue persistence. Enhance UI with new metrics and visualizations for improved user experience.
This commit is contained in:
parent
a6bf055828
commit
1dc8f4cf7a
1
QUEUE_SCHEDULER_IMPLEMENTATION.md
Normal file
1
QUEUE_SCHEDULER_IMPLEMENTATION.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
500
pages/notification/log-audit/analytics.vue
Normal file
500
pages/notification/log-audit/analytics.vue
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page Info Card -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-bar-chart"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Analytics Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Visual analytics and metrics for notification performance, delivery rates, and user engagement trends.
|
||||||
|
Monitor key performance indicators and analyze patterns to optimize your notification strategy.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Key Metrics Summary -->
|
||||||
|
<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 keyMetrics"
|
||||||
|
: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-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
||||||
|
>
|
||||||
|
<Icon class="text-primary text-3xl" :name="metric.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||||
|
{{ metric.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ metric.title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center mt-1">
|
||||||
|
<Icon
|
||||||
|
:name="metric.trend === 'up' ? 'ic:outline-trending-up' : 'ic:outline-trending-down'"
|
||||||
|
:class="metric.trend === 'up' ? 'text-green-500' : 'text-red-500'"
|
||||||
|
class="text-sm mr-1"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="metric.trend === 'up' ? 'text-green-600' : 'text-red-600'"
|
||||||
|
class="text-xs font-medium"
|
||||||
|
>
|
||||||
|
{{ metric.change }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time Range Filter -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #body>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Analytics Period</h3>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="selectedPeriod"
|
||||||
|
:options="periodOptions"
|
||||||
|
outer-class="mb-0"
|
||||||
|
@input="updateAnalytics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="primary-outline" size="sm" @click="refreshAnalytics">
|
||||||
|
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh Data
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Main Analytics Charts -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-bar-chart" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Delivery Rate Analysis</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="exportChart('delivery')">
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1"/> Export
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-64 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="ic:outline-assessment" class="text-4xl text-gray-400 mb-2"/>
|
||||||
|
<p class="text-gray-500">Chart Implementation Pending</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Line chart showing delivery success over time</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid grid-cols-3 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<div class="text-2xl font-bold text-green-600">98.2%</div>
|
||||||
|
<div class="text-sm text-gray-600">Success Rate</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-2xl font-bold text-red-600">1.5%</div>
|
||||||
|
<div class="text-sm text-gray-600">Failed Rate</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-2xl font-bold text-yellow-600">0.3%</div>
|
||||||
|
<div class="text-sm text-gray-600">Bounce Rate</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-open-in-new" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Open Rate Metrics</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="exportChart('openrate')">
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1"/> Export
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-64 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="ic:outline-assessment" class="text-4xl text-gray-400 mb-2"/>
|
||||||
|
<p class="text-gray-500">Chart Implementation Pending</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Pie chart showing open rates by channel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
|
||||||
|
<span class="text-sm">Email</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">24.5%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||||
|
<span class="text-sm">Push Notification</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">18.7%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-3 h-3 bg-yellow-500 rounded-full mr-2"></div>
|
||||||
|
<span class="text-sm">SMS</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">45.2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Engagement Trends -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-trending-up" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">User Engagement Trends</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="selectedChannel"
|
||||||
|
:options="channelFilterOptions"
|
||||||
|
outer-class="mb-0"
|
||||||
|
@input="updateEngagementChart"
|
||||||
|
/>
|
||||||
|
<rs-button variant="outline" size="sm" @click="exportChart('engagement')">
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1"/> Export
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-80 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="ic:outline-assessment" class="text-4xl text-gray-400 mb-2"/>
|
||||||
|
<p class="text-gray-500">Chart Implementation Pending</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Multi-line chart showing engagement metrics over time</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Performance Breakdown -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-schedule" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Peak Activity Hours</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-48 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded mb-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="ic:outline-access-time" class="text-3xl text-gray-400 mb-2"/>
|
||||||
|
<p class="text-gray-500">Heatmap Implementation Pending</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xl font-bold text-primary">2:00 PM</div>
|
||||||
|
<div class="text-sm text-gray-600">Peak Hour</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xl font-bold text-primary">3:47</div>
|
||||||
|
<div class="text-sm text-gray-600">Avg Response Time (s)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-device-hub" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Channel Performance</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="channel in channelPerformance"
|
||||||
|
:key="channel.name"
|
||||||
|
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon :name="channel.icon" class="mr-2 text-primary"/>
|
||||||
|
<span class="font-medium">{{ channel.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-primary">{{ channel.successRate }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
class="bg-primary h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: channel.successRate + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||||
|
<span>{{ channel.sent }} sent</span>
|
||||||
|
<span>{{ channel.failed }} failed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Analytics Events -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-insights" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Analytics Events</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="navigateTo('/notification/log-audit/logs')">
|
||||||
|
View All Logs
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(event, index) in recentEvents"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': event.type === 'success',
|
||||||
|
'bg-yellow-500': event.type === 'warning',
|
||||||
|
'bg-red-500': event.type === 'error',
|
||||||
|
'bg-blue-500': event.type === 'info',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ event.title }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ event.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium">{{ event.value }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ event.time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Analytics Dashboard",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs & Audit Trail",
|
||||||
|
path: "/notification/log-audit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Analytics",
|
||||||
|
path: "/notification/log-audit/analytics",
|
||||||
|
type: "current"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// Time period selection
|
||||||
|
const selectedPeriod = ref('7d')
|
||||||
|
const periodOptions = [
|
||||||
|
{ label: 'Last 24 Hours', value: '1d' },
|
||||||
|
{ label: 'Last 7 Days', value: '7d' },
|
||||||
|
{ label: 'Last 30 Days', value: '30d' },
|
||||||
|
{ label: 'Last 90 Days', value: '90d' },
|
||||||
|
{ label: 'Last 12 Months', value: '12m' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Channel filter for engagement chart
|
||||||
|
const selectedChannel = ref('all')
|
||||||
|
const channelFilterOptions = [
|
||||||
|
{ label: 'All Channels', value: 'all' },
|
||||||
|
{ label: 'Email', value: 'email' },
|
||||||
|
{ label: 'SMS', value: 'sms' },
|
||||||
|
{ label: 'Push Notification', value: 'push' },
|
||||||
|
{ label: 'Webhook', value: 'webhook' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Key metrics data
|
||||||
|
const keyMetrics = computed(() => [
|
||||||
|
{
|
||||||
|
title: "Total Sent",
|
||||||
|
value: "127,847",
|
||||||
|
icon: "ic:outline-send",
|
||||||
|
trend: "up",
|
||||||
|
change: "+12.5%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Success Rate",
|
||||||
|
value: "98.2%",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
trend: "up",
|
||||||
|
change: "+2.1%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Open Rate",
|
||||||
|
value: "24.7%",
|
||||||
|
icon: "ic:outline-open-in-new",
|
||||||
|
trend: "down",
|
||||||
|
change: "-1.3%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Click Rate",
|
||||||
|
value: "4.8%",
|
||||||
|
icon: "ic:outline-touch-app",
|
||||||
|
trend: "up",
|
||||||
|
change: "+0.7%"
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Channel performance data
|
||||||
|
const channelPerformance = ref([
|
||||||
|
{
|
||||||
|
name: "Email",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
successRate: 98,
|
||||||
|
sent: "45,230",
|
||||||
|
failed: "912"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SMS",
|
||||||
|
icon: "ic:outline-sms",
|
||||||
|
successRate: 97,
|
||||||
|
sent: "32,156",
|
||||||
|
failed: "987"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Push Notification",
|
||||||
|
icon: "ic:outline-notifications",
|
||||||
|
successRate: 99,
|
||||||
|
sent: "89,421",
|
||||||
|
failed: "645"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Webhook",
|
||||||
|
icon: "ic:outline-webhook",
|
||||||
|
successRate: 95,
|
||||||
|
sent: "12,340",
|
||||||
|
failed: "617"
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Recent analytics events
|
||||||
|
const recentEvents = ref([
|
||||||
|
{
|
||||||
|
title: "High Open Rate Detected",
|
||||||
|
description: "Email campaign achieved 45% open rate",
|
||||||
|
value: "45.2%",
|
||||||
|
time: "2 minutes ago",
|
||||||
|
type: "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SMS Delivery Spike",
|
||||||
|
description: "SMS volume increased by 300%",
|
||||||
|
value: "+300%",
|
||||||
|
time: "15 minutes ago",
|
||||||
|
type: "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Push Notification Failure",
|
||||||
|
description: "Batch delivery failed for iOS devices",
|
||||||
|
value: "2,456 failed",
|
||||||
|
time: "1 hour ago",
|
||||||
|
type: "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Webhook Timeout Warning",
|
||||||
|
description: "Response time exceeded threshold",
|
||||||
|
value: "8.5s avg",
|
||||||
|
time: "2 hours ago",
|
||||||
|
type: "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Peak Activity Period",
|
||||||
|
description: "Highest activity recorded today",
|
||||||
|
value: "15,420/hr",
|
||||||
|
time: "3 hours ago",
|
||||||
|
type: "info"
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const updateAnalytics = () => {
|
||||||
|
console.log('Updating analytics for period:', selectedPeriod.value)
|
||||||
|
// Simulate data refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateEngagementChart = () => {
|
||||||
|
console.log('Updating engagement chart for channel:', selectedChannel.value)
|
||||||
|
// Simulate chart update
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshAnalytics = () => {
|
||||||
|
console.log('Refreshing analytics data...')
|
||||||
|
// Simulate refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportChart = (chartType) => {
|
||||||
|
console.log(`Exporting ${chartType} chart...`)
|
||||||
|
alert(`Exporting ${chartType} chart. (Implementation pending)`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// Custom styles for FormKit consistency
|
||||||
|
:deep(.formkit-outer) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-input) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,7 +1,844 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page Info Card -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-assessment"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Notification Logs & Audit Trail</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
This module provides comprehensive logging and auditing capabilities for all
|
||||||
|
notification activities. Track deliveries, monitor performance, analyze trends,
|
||||||
|
and ensure compliance with detailed audit trails.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Summary Stats -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(item, index) in summaryStats"
|
||||||
|
: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-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
||||||
|
>
|
||||||
|
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||||
|
{{ item.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Cards -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(feature, index) in features"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg cursor-pointer"
|
||||||
|
@click="navigateTo(feature.path)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" :name="feature.icon"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">{{ feature.title }}</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600 mb-4">{{ feature.description }}</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(useCase, idx) in feature.useCases"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-start"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="mr-2 mt-1 text-green-500 text-sm"
|
||||||
|
name="ic:outline-check-circle"
|
||||||
|
></Icon>
|
||||||
|
<span class="text-sm text-gray-700">{{ useCase }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<rs-button variant="outline" size="sm">
|
||||||
|
<Icon class="mr-1" name="ic:outline-arrow-forward"></Icon>
|
||||||
|
Open
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audit Logs 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-list-alt"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Audit Logs</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="navigateTo('/notification/log-audit/logs')"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4"
|
||||||
|
>
|
||||||
|
<div class="xl:col-span-2">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||||
|
>Date Range</label
|
||||||
|
>
|
||||||
|
<rs-button variant="primary-outline" class="w-full justify-start">
|
||||||
|
<Icon name="ic:outline-date-range" class="mr-2" />
|
||||||
|
{{
|
||||||
|
selectedDateRange.start && selectedDateRange.end
|
||||||
|
? `${formatDate(selectedDateRange.start)} - ${formatDate(
|
||||||
|
selectedDateRange.end
|
||||||
|
)}`
|
||||||
|
: "Select date range"
|
||||||
|
}}
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="User or Actor"
|
||||||
|
v-model="filters.actor"
|
||||||
|
placeholder="Enter user or actor"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Channel"
|
||||||
|
v-model="filters.channel"
|
||||||
|
placeholder="Select channel"
|
||||||
|
:options="channelOptionsForFormKit"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Status"
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="Select status"
|
||||||
|
:options="statusOptionsForFormKit"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="search"
|
||||||
|
label="Search Content"
|
||||||
|
v-model="filters.keyword"
|
||||||
|
placeholder="Search in logs..."
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<rs-button @click="applyFilters" variant="primary">
|
||||||
|
<Icon name="ic:outline-search" class="mr-1" /> Apply Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Table -->
|
||||||
|
<rs-table
|
||||||
|
v-if="filteredLogs && filteredLogs.length > 0"
|
||||||
|
:data="filteredLogs.slice(0, 10)"
|
||||||
|
:field="logTableFields"
|
||||||
|
:options="{
|
||||||
|
variant: 'default',
|
||||||
|
striped: true,
|
||||||
|
borderless: false,
|
||||||
|
hover: true,
|
||||||
|
}"
|
||||||
|
:options-advanced="{
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
responsive: true,
|
||||||
|
outsideBorder: true,
|
||||||
|
}"
|
||||||
|
advanced
|
||||||
|
>
|
||||||
|
<template #timestamp="{ value }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="font-medium">{{ formatDate(value.timestamp, true) }}</div>
|
||||||
|
<div class="text-gray-500 text-xs">
|
||||||
|
{{ new Date(value.timestamp).toLocaleTimeString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<template #status="{ value }">
|
||||||
|
<rs-badge
|
||||||
|
:variant="
|
||||||
|
value.status === 'Sent'
|
||||||
|
? 'success'
|
||||||
|
: value.status === 'Failed'
|
||||||
|
? 'danger'
|
||||||
|
: value.status === 'Opened'
|
||||||
|
? 'info'
|
||||||
|
: value.status === 'Queued'
|
||||||
|
? 'warning'
|
||||||
|
: 'secondary'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ value.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<template #actions="{ value }">
|
||||||
|
<rs-button variant="primary-text" size="sm" @click="viewLogDetails(value)">
|
||||||
|
<Icon name="ic:outline-visibility" class="mr-1" /> View Details
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
</rs-table>
|
||||||
|
|
||||||
|
<div v-else class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto" />
|
||||||
|
<p>No log entries found matching your filters.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<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-history"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Activity</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="navigateTo('/notification/log-audit/monitoring')"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(activity, index) in recentActivity"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500':
|
||||||
|
activity.status === 'sent' ||
|
||||||
|
activity.status === 'created' ||
|
||||||
|
activity.status === 'opened',
|
||||||
|
'bg-yellow-500':
|
||||||
|
activity.status === 'queued' || activity.status === 'updated',
|
||||||
|
'bg-red-500': activity.status === 'failed',
|
||||||
|
'bg-blue-500': activity.status === 'bounced',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ activity.action }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ activity.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium capitalize">{{ activity.status }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ activity.time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Log Details Modal -->
|
||||||
|
<rs-modal
|
||||||
|
v-model="isLogDetailModalOpen"
|
||||||
|
title="Log Entry Details"
|
||||||
|
size="lg"
|
||||||
|
:overlay-close="true"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedLog" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Log ID</label>
|
||||||
|
<p class="font-mono text-sm">{{ selectedLog.id }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||||
|
>Timestamp</label
|
||||||
|
>
|
||||||
|
<p class="text-sm">{{ formatDate(selectedLog.timestamp, true) }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Action</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.action }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Actor</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.actor }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.channel }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||||
|
<rs-badge
|
||||||
|
:variant="
|
||||||
|
selectedLog.status === 'Sent'
|
||||||
|
? 'success'
|
||||||
|
: selectedLog.status === 'Failed'
|
||||||
|
? 'danger'
|
||||||
|
: selectedLog.status === 'Opened'
|
||||||
|
? 'info'
|
||||||
|
: selectedLog.status === 'Queued'
|
||||||
|
? 'warning'
|
||||||
|
: 'secondary'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ selectedLog.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||||
|
>Source IP</label
|
||||||
|
>
|
||||||
|
<p class="text-sm font-mono">{{ selectedLog.sourceIp }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Details</label>
|
||||||
|
<p class="text-sm p-3 bg-gray-50 dark:bg-gray-800 rounded">
|
||||||
|
{{ selectedLog.details }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedLog.status === 'Failed'">
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<h4 class="text-lg font-medium text-red-600 dark:text-red-400 mb-3">
|
||||||
|
Error Information
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||||
|
>Error Code</label
|
||||||
|
>
|
||||||
|
<p class="text-sm font-mono text-red-600 dark:text-red-400">
|
||||||
|
{{ selectedLog.errorCode }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||||
|
>Error Message</label
|
||||||
|
>
|
||||||
|
<p class="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{{ selectedLog.errorMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-2"
|
||||||
|
>Failed Payload</label
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs max-h-32 overflow-y-auto"
|
||||||
|
>{{ selectedLog.failedPayload }}</pre
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||||
|
>Troubleshooting</label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded border-l-4 border-yellow-400"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
|
{{ selectedLog.troubleshooting }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<rs-button @click="isLogDetailModalOpen = false" variant="primary-outline">
|
||||||
|
Close
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Notification Logs & Audit Trail",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs & Audit Trail",
|
||||||
|
path: "/notification/log-audit",
|
||||||
|
type: "current",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// --- Log Filters ---
|
||||||
|
const selectedDateRange = ref({ start: null, end: null });
|
||||||
|
const filters = ref({
|
||||||
|
actor: "",
|
||||||
|
channel: null,
|
||||||
|
status: null,
|
||||||
|
keyword: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelOptions = ["Email", "SMS", "Push Notification", "Webhook"];
|
||||||
|
const statusOptions = [
|
||||||
|
"Sent",
|
||||||
|
"Failed",
|
||||||
|
"Bounced",
|
||||||
|
"Opened",
|
||||||
|
"Queued",
|
||||||
|
"Created",
|
||||||
|
"Updated",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Prepare options for FormKit select
|
||||||
|
const channelOptionsForFormKit = computed(() => [
|
||||||
|
{ label: "All Channels", value: null },
|
||||||
|
...channelOptions.map((opt) => ({ label: opt, value: opt })),
|
||||||
|
]);
|
||||||
|
const statusOptionsForFormKit = computed(() => [
|
||||||
|
{ label: "All Status", value: null },
|
||||||
|
...statusOptions.map((opt) => ({ label: opt, value: opt })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formatDate = (date, includeTime = false) => {
|
||||||
|
if (!date) return "";
|
||||||
|
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||||
|
if (includeTime) {
|
||||||
|
options.hour = "2-digit";
|
||||||
|
options.minute = "2-digit";
|
||||||
|
}
|
||||||
|
return new Date(date).toLocaleDateString(undefined, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Log Data & Table ---
|
||||||
|
const dummyLogs = ref([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
timestamp: new Date("2023-10-01T10:00:00Z"),
|
||||||
|
action: "Notification Created",
|
||||||
|
actor: "Admin User",
|
||||||
|
channel: "Email",
|
||||||
|
status: "Created",
|
||||||
|
sourceIp: "192.168.1.100",
|
||||||
|
details: "Welcome email template for new user signup.",
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
timestamp: new Date("2023-10-01T10:05:00Z"),
|
||||||
|
action: "Notification Sent",
|
||||||
|
actor: "System",
|
||||||
|
channel: "Email",
|
||||||
|
status: "Sent",
|
||||||
|
sourceIp: "N/A",
|
||||||
|
details: "To: user@example.com, Subject: Important Account Update",
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
timestamp: new Date("2023-10-01T10:15:00Z"),
|
||||||
|
action: "Delivery Failed",
|
||||||
|
actor: "System",
|
||||||
|
channel: "SMS",
|
||||||
|
status: "Failed",
|
||||||
|
sourceIp: "N/A",
|
||||||
|
details: "To: +1234560000 (Invalid Number)",
|
||||||
|
errorCode: "SMS-404",
|
||||||
|
errorMessage: "The recipient phone number is not valid or not reachable.",
|
||||||
|
failedPayload: JSON.stringify({
|
||||||
|
to: "+1234560000",
|
||||||
|
message: "Your verification code is 789012.",
|
||||||
|
}),
|
||||||
|
troubleshooting:
|
||||||
|
"Check number validity and country code. Ensure the number can receive SMS.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
timestamp: new Date("2023-10-02T11:00:00Z"),
|
||||||
|
action: "Notification Opened",
|
||||||
|
actor: "user@example.com",
|
||||||
|
channel: "Email",
|
||||||
|
status: "Opened",
|
||||||
|
sourceIp: "203.0.113.45",
|
||||||
|
details: "Subject: Important Account Update - Opened via Webmail on Chrome.",
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
timestamp: new Date("2023-10-03T14:30:00Z"),
|
||||||
|
action: "Template Updated",
|
||||||
|
actor: "marketing_mgr",
|
||||||
|
channel: "N/A",
|
||||||
|
status: "Updated",
|
||||||
|
sourceIp: "192.168.1.150",
|
||||||
|
details: 'Promotional Email Template "SummerSale2023" updated with new banner.',
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
timestamp: new Date("2023-10-04T09:00:00Z"),
|
||||||
|
action: "Notification Queued",
|
||||||
|
actor: "System",
|
||||||
|
channel: "Push Notification",
|
||||||
|
status: "Queued",
|
||||||
|
sourceIp: "N/A",
|
||||||
|
details: 'Targeting segment: "Active Android Users"',
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
timestamp: new Date("2023-10-04T09:05:00Z"),
|
||||||
|
action: "Notification Sent",
|
||||||
|
actor: "System",
|
||||||
|
channel: "Push Notification",
|
||||||
|
status: "Sent",
|
||||||
|
sourceIp: "N/A",
|
||||||
|
details: "Successfully sent to 5,032 devices.",
|
||||||
|
errorCode: null,
|
||||||
|
errorMessage: null,
|
||||||
|
failedPayload: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
timestamp: new Date("2023-10-05T16:20:00Z"),
|
||||||
|
action: "Delivery Bounced",
|
||||||
|
actor: "System",
|
||||||
|
channel: "Email",
|
||||||
|
status: "Bounced",
|
||||||
|
sourceIp: "N/A",
|
||||||
|
details: "To: nonexist@example.com, Hard Bounce",
|
||||||
|
errorCode: "SMTP-550",
|
||||||
|
errorMessage: "User mailbox does not exist.",
|
||||||
|
failedPayload: JSON.stringify({
|
||||||
|
to: "nonexist@example.com",
|
||||||
|
subject: "Your Weekly Digest",
|
||||||
|
}),
|
||||||
|
troubleshooting: "Remove email from list. Verify email collection process.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add more dummy logs for pagination and filtering
|
||||||
|
...Array.from({ length: 22 }, (_, i) => ({
|
||||||
|
id: (i + 9).toString(),
|
||||||
|
timestamp: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
|
||||||
|
action: [
|
||||||
|
"Notification Created",
|
||||||
|
"Notification Sent",
|
||||||
|
"Delivery Failed",
|
||||||
|
"Notification Opened",
|
||||||
|
"Template Updated",
|
||||||
|
"Notification Queued",
|
||||||
|
"Delivery Bounced",
|
||||||
|
][Math.floor(Math.random() * 7)],
|
||||||
|
actor: [
|
||||||
|
"Admin User",
|
||||||
|
"System",
|
||||||
|
"john.doe@example.com",
|
||||||
|
"jane.doe@example.com",
|
||||||
|
"support_agent",
|
||||||
|
"marketing_mgr",
|
||||||
|
][Math.floor(Math.random() * 6)],
|
||||||
|
channel: channelOptions[Math.floor(Math.random() * channelOptions.length)],
|
||||||
|
status: statusOptions[Math.floor(Math.random() * statusOptions.length)],
|
||||||
|
sourceIp: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
|
||||||
|
details: `Log entry detail for event id ${
|
||||||
|
i + 9
|
||||||
|
}. This notification was processed with standard parameters.`,
|
||||||
|
errorCode:
|
||||||
|
Math.random() > 0.85 ? `ERR-${Math.floor(Math.random() * 100) + 200}` : null,
|
||||||
|
errorMessage:
|
||||||
|
Math.random() > 0.85
|
||||||
|
? "A simulated intermittent error occurred during processing."
|
||||||
|
: null,
|
||||||
|
failedPayload:
|
||||||
|
Math.random() > 0.85
|
||||||
|
? JSON.stringify({
|
||||||
|
data: "some example payload data",
|
||||||
|
attempt: Math.floor(Math.random() * 3) + 1,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
troubleshooting:
|
||||||
|
Math.random() > 0.85
|
||||||
|
? "Retry the operation. If persistent, check downstream service status."
|
||||||
|
: null,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// This computed property will be passed to RsTable's `data` prop
|
||||||
|
const filteredLogs = computed(() => {
|
||||||
|
return dummyLogs.value.filter((log) => {
|
||||||
|
const logDate = new Date(log.timestamp);
|
||||||
|
|
||||||
|
let dateMatch = true;
|
||||||
|
if (
|
||||||
|
selectedDateRange.value &&
|
||||||
|
selectedDateRange.value.start &&
|
||||||
|
selectedDateRange.value.end
|
||||||
|
) {
|
||||||
|
const startDate = new Date(selectedDateRange.value.start);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
const endDate = new Date(selectedDateRange.value.end);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
dateMatch = logDate >= startDate && logDate <= endDate;
|
||||||
|
} else if (selectedDateRange.value && selectedDateRange.value.start) {
|
||||||
|
const startDate = new Date(selectedDateRange.value.start);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
dateMatch = logDate >= startDate;
|
||||||
|
} else if (selectedDateRange.value && selectedDateRange.value.end) {
|
||||||
|
const endDate = new Date(selectedDateRange.value.end);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
dateMatch = logDate <= endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorMatch =
|
||||||
|
!filters.value.actor ||
|
||||||
|
(log.actor && log.actor.toLowerCase().includes(filters.value.actor.toLowerCase()));
|
||||||
|
const channelMatch = !filters.value.channel || log.channel === filters.value.channel;
|
||||||
|
const statusMatch = !filters.value.status || log.status === filters.value.status;
|
||||||
|
const keywordMatch =
|
||||||
|
!filters.value.keyword ||
|
||||||
|
(log.details &&
|
||||||
|
log.details.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
||||||
|
(log.action &&
|
||||||
|
log.action.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
||||||
|
(log.errorMessage &&
|
||||||
|
log.errorMessage.toLowerCase().includes(filters.value.keyword.toLowerCase()));
|
||||||
|
|
||||||
|
return dateMatch && actorMatch && channelMatch && statusMatch && keywordMatch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fields for RsTable
|
||||||
|
const logTableFields = ["timestamp", "action", "actor", "channel", "status", "actions"];
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
console.log("Applying filters with values:", filters.value, selectedDateRange.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Log Detail Modal ---
|
||||||
|
const isLogDetailModalOpen = ref(false);
|
||||||
|
const selectedLog = ref(null);
|
||||||
|
|
||||||
|
const viewLogDetails = (log) => {
|
||||||
|
selectedLog.value = log;
|
||||||
|
isLogDetailModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Summary stats for quick overview
|
||||||
|
const summaryStats = computed(() => [
|
||||||
|
{
|
||||||
|
title: "Total Log Entries",
|
||||||
|
value: filteredLogs.value.length.toLocaleString(),
|
||||||
|
icon: "ic:outline-list-alt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Failed Deliveries",
|
||||||
|
value: filteredLogs.value
|
||||||
|
.filter((log) => log.status === "Failed")
|
||||||
|
.length.toLocaleString(),
|
||||||
|
icon: "ic:outline-error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Successful Deliveries",
|
||||||
|
value: filteredLogs.value
|
||||||
|
.filter((log) => log.status === "Sent")
|
||||||
|
.length.toLocaleString(),
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Success Rate",
|
||||||
|
value:
|
||||||
|
filteredLogs.value.length > 0
|
||||||
|
? Math.round(
|
||||||
|
(filteredLogs.value.filter((log) => log.status === "Sent").length /
|
||||||
|
filteredLogs.value.length) *
|
||||||
|
100
|
||||||
|
) + "%"
|
||||||
|
: "0%",
|
||||||
|
icon: "ic:outline-trending-up",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Feature cards data for navigation
|
||||||
|
const features = ref([
|
||||||
|
{
|
||||||
|
title: "Analytics Dashboard",
|
||||||
|
description:
|
||||||
|
"Visual analytics and metrics for notification performance, delivery rates, and user engagement trends.",
|
||||||
|
icon: "ic:outline-bar-chart",
|
||||||
|
path: "/notification/log-audit/analytics",
|
||||||
|
useCases: [
|
||||||
|
"Track delivery rates and success metrics",
|
||||||
|
"Monitor open rates and click-through rates",
|
||||||
|
"Analyze engagement trends over time",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Real-Time Monitoring",
|
||||||
|
description:
|
||||||
|
"Live monitoring of notification system performance with real-time alerts and system health indicators.",
|
||||||
|
icon: "ic:outline-monitor",
|
||||||
|
path: "/notification/log-audit/monitoring",
|
||||||
|
useCases: [
|
||||||
|
"Monitor ongoing notification activity",
|
||||||
|
"Track current system load and performance",
|
||||||
|
"Receive alerts for error spikes and failures",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Reports & Export",
|
||||||
|
description:
|
||||||
|
"Generate comprehensive reports and export log data for compliance, auditing, and analysis purposes.",
|
||||||
|
icon: "ic:outline-file-download",
|
||||||
|
path: "/notification/log-audit/reports",
|
||||||
|
useCases: [
|
||||||
|
"Export logs in CSV, PDF, and Excel formats",
|
||||||
|
"Generate GDPR and SOC2 compliance reports",
|
||||||
|
"Create custom audit reports for stakeholders",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Recent activity data (top 5 most recent logs)
|
||||||
|
const recentActivity = computed(() => {
|
||||||
|
return filteredLogs.value
|
||||||
|
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((log) => ({
|
||||||
|
action: log.action,
|
||||||
|
description: log.details,
|
||||||
|
status: log.status.toLowerCase(),
|
||||||
|
time: formatTimeAgo(log.timestamp),
|
||||||
|
actor: log.actor,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatTimeAgo = (timestamp) => {
|
||||||
|
const now = new Date();
|
||||||
|
const time = new Date(timestamp);
|
||||||
|
const diffInMinutes = Math.floor((now - time) / (1000 * 60));
|
||||||
|
|
||||||
|
if (diffInMinutes < 1) return "Just now";
|
||||||
|
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
|
||||||
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
|
||||||
|
return `${Math.floor(diffInMinutes / 1440)} days ago`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// Custom styles for FormKit consistency
|
||||||
|
:deep(.formkit-outer) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-input) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge component styles (if RsBadge doesn't exist, these can be adjusted)
|
||||||
|
.rs-badge {
|
||||||
|
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-success {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-danger {
|
||||||
|
@apply bg-red-100 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-warning {
|
||||||
|
@apply bg-yellow-100 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-info {
|
||||||
|
@apply bg-blue-100 text-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-secondary {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
500
pages/notification/log-audit/logs.vue
Normal file
500
pages/notification/log-audit/logs.vue
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page Info Card -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-list-alt"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Audit Logs</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Comprehensive view of all notification logs and audit trail entries.
|
||||||
|
Filter, search, and analyze notification activities with detailed information.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Summary Stats -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(item, index) in summaryStats"
|
||||||
|
: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-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
||||||
|
>
|
||||||
|
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||||
|
{{ item.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Card -->
|
||||||
|
<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-history"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">All Audit Logs</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-button variant="secondary-outline" size="sm" @click="exportData('csv')">
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1"/> Export
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="primary-outline" size="sm" @click="refreshLogs">
|
||||||
|
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||||
|
<div class="xl:col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Date Range</label>
|
||||||
|
<rs-button variant="primary-outline" class="w-full justify-start">
|
||||||
|
<Icon name="ic:outline-date-range" class="mr-2"/>
|
||||||
|
{{ selectedDateRange.start && selectedDateRange.end ?
|
||||||
|
`${formatDate(selectedDateRange.start)} - ${formatDate(selectedDateRange.end)}` :
|
||||||
|
'Select date range' }}
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="User or Actor"
|
||||||
|
v-model="filters.actor"
|
||||||
|
placeholder="Enter user or actor"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Channel"
|
||||||
|
v-model="filters.channel"
|
||||||
|
placeholder="Select channel"
|
||||||
|
:options="channelOptionsForFormKit"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Status"
|
||||||
|
v-model="filters.status"
|
||||||
|
placeholder="Select status"
|
||||||
|
:options="statusOptionsForFormKit"
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="search"
|
||||||
|
label="Search Content"
|
||||||
|
v-model="filters.keyword"
|
||||||
|
placeholder="Search in logs..."
|
||||||
|
outer-class="mb-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mt-4">
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
Showing {{ filteredLogs.length }} of {{ dummyLogs.length }} entries
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<rs-button @click="clearFilters" variant="secondary-outline" size="sm">
|
||||||
|
<Icon name="ic:outline-clear" class="mr-1"/> Clear Filters
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="applyFilters" variant="primary">
|
||||||
|
<Icon name="ic:outline-search" class="mr-1"/> Apply Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Table -->
|
||||||
|
<rs-table
|
||||||
|
v-if="filteredLogs && filteredLogs.length > 0"
|
||||||
|
:data="filteredLogs"
|
||||||
|
:field="logTableFields"
|
||||||
|
:options="{
|
||||||
|
variant: 'default',
|
||||||
|
striped: true,
|
||||||
|
borderless: false,
|
||||||
|
hover: true
|
||||||
|
}"
|
||||||
|
:options-advanced="{
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
responsive: true,
|
||||||
|
outsideBorder: true
|
||||||
|
}"
|
||||||
|
advanced
|
||||||
|
>
|
||||||
|
<template #timestamp="{ value }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="font-medium">{{ formatDate(value.timestamp, true) }}</div>
|
||||||
|
<div class="text-gray-500 text-xs">{{ new Date(value.timestamp).toLocaleTimeString() }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #status="{ value }">
|
||||||
|
<rs-badge
|
||||||
|
:variant="value.status === 'Sent' ? 'success' :
|
||||||
|
value.status === 'Failed' ? 'danger' :
|
||||||
|
value.status === 'Opened' ? 'info' :
|
||||||
|
value.status === 'Queued' ? 'warning' : 'secondary'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ value.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ value }">
|
||||||
|
<rs-button variant="primary-text" size="sm" @click="viewLogDetails(value)">
|
||||||
|
<Icon name="ic:outline-visibility" class="mr-1"/> View Details
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
</rs-table>
|
||||||
|
|
||||||
|
<div v-else class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto"/>
|
||||||
|
<p>No log entries found matching your filters.</p>
|
||||||
|
<rs-button variant="primary-outline" @click="clearFilters" class="mt-4">
|
||||||
|
Clear Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Log Details Modal -->
|
||||||
|
<rs-modal
|
||||||
|
v-model="isLogDetailModalOpen"
|
||||||
|
title="Log Entry Details"
|
||||||
|
size="lg"
|
||||||
|
:overlay-close="true"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedLog" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Log ID</label>
|
||||||
|
<p class="font-mono text-sm">{{ selectedLog.id }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Timestamp</label>
|
||||||
|
<p class="text-sm">{{ formatDate(selectedLog.timestamp, true) }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Action</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.action }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Actor</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.actor }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
|
||||||
|
<p class="text-sm">{{ selectedLog.channel }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||||
|
<rs-badge
|
||||||
|
:variant="selectedLog.status === 'Sent' ? 'success' :
|
||||||
|
selectedLog.status === 'Failed' ? 'danger' :
|
||||||
|
selectedLog.status === 'Opened' ? 'info' :
|
||||||
|
selectedLog.status === 'Queued' ? 'warning' : 'secondary'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ selectedLog.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Source IP</label>
|
||||||
|
<p class="text-sm font-mono">{{ selectedLog.sourceIp }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Details</label>
|
||||||
|
<p class="text-sm p-3 bg-gray-50 dark:bg-gray-800 rounded">{{ selectedLog.details }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedLog.status === 'Failed'">
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<h4 class="text-lg font-medium text-red-600 dark:text-red-400 mb-3">Error Information</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Error Code</label>
|
||||||
|
<p class="text-sm font-mono text-red-600 dark:text-red-400">{{ selectedLog.errorCode }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Error Message</label>
|
||||||
|
<p class="text-sm text-red-600 dark:text-red-400">{{ selectedLog.errorMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-2">Failed Payload</label>
|
||||||
|
<pre class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs max-h-32 overflow-y-auto">{{ selectedLog.failedPayload }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-500 mb-1">Troubleshooting</label>
|
||||||
|
<div class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded border-l-4 border-yellow-400">
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">{{ selectedLog.troubleshooting }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<rs-button @click="isLogDetailModalOpen = false" variant="primary-outline">
|
||||||
|
Close
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Audit Logs",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs & Audit Trail",
|
||||||
|
path: "/notification/log-audit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Audit Logs",
|
||||||
|
path: "/notification/log-audit/logs",
|
||||||
|
type: "current"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// --- Log Filters ---
|
||||||
|
const selectedDateRange = ref({ start: null, end: null });
|
||||||
|
const filters = ref({
|
||||||
|
actor: '',
|
||||||
|
channel: null,
|
||||||
|
status: null,
|
||||||
|
keyword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const channelOptions = ['Email', 'SMS', 'Push Notification', 'Webhook']
|
||||||
|
const statusOptions = ['Sent', 'Failed', 'Bounced', 'Opened', 'Queued', 'Created', 'Updated']
|
||||||
|
|
||||||
|
// Prepare options for FormKit select
|
||||||
|
const channelOptionsForFormKit = computed(() => [
|
||||||
|
{ label: 'All Channels', value: null },
|
||||||
|
...channelOptions.map(opt => ({ label: opt, value: opt }))
|
||||||
|
])
|
||||||
|
const statusOptionsForFormKit = computed(() => [
|
||||||
|
{ label: 'All Status', value: null },
|
||||||
|
...statusOptions.map(opt => ({ label: opt, value: opt }))
|
||||||
|
])
|
||||||
|
|
||||||
|
const formatDate = (date, includeTime = false) => {
|
||||||
|
if (!date) return ''
|
||||||
|
const options = { year: 'numeric', month: 'short', day: 'numeric' }
|
||||||
|
if (includeTime) {
|
||||||
|
options.hour = '2-digit';
|
||||||
|
options.minute = '2-digit';
|
||||||
|
}
|
||||||
|
return new Date(date).toLocaleDateString(undefined, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Log Data & Table ---
|
||||||
|
const dummyLogs = ref([
|
||||||
|
{ id: '1', timestamp: new Date('2023-10-01T10:00:00Z'), action: 'Notification Created', actor: 'Admin User', channel: 'Email', status: 'Created', sourceIp: '192.168.1.100', details: 'Welcome email template for new user signup.', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '2', timestamp: new Date('2023-10-01T10:05:00Z'), action: 'Notification Sent', actor: 'System', channel: 'Email', status: 'Sent', sourceIp: 'N/A', details: 'To: user@example.com, Subject: Important Account Update', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '3', timestamp: new Date('2023-10-01T10:15:00Z'), action: 'Delivery Failed', actor: 'System', channel: 'SMS', status: 'Failed', sourceIp: 'N/A', details: 'To: +1234560000 (Invalid Number)', errorCode: 'SMS-404', errorMessage: 'The recipient phone number is not valid or not reachable.', failedPayload: JSON.stringify({ to: "+1234560000", message: "Your verification code is 789012."}), troubleshooting: 'Check number validity and country code. Ensure the number can receive SMS.' },
|
||||||
|
{ id: '4', timestamp: new Date('2023-10-02T11:00:00Z'), action: 'Notification Opened', actor: 'user@example.com', channel: 'Email', status: 'Opened', sourceIp: '203.0.113.45', details: 'Subject: Important Account Update - Opened via Webmail on Chrome.', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '5', timestamp: new Date('2023-10-03T14:30:00Z'), action: 'Template Updated', actor: 'marketing_mgr', channel: 'N/A', status: 'Updated', sourceIp: '192.168.1.150', details: 'Promotional Email Template "SummerSale2023" updated with new banner.', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '6', timestamp: new Date('2023-10-04T09:00:00Z'), action: 'Notification Queued', actor: 'System', channel: 'Push Notification', status: 'Queued', sourceIp: 'N/A', details: 'Targeting segment: "Active Android Users"', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '7', timestamp: new Date('2023-10-04T09:05:00Z'), action: 'Notification Sent', actor: 'System', channel: 'Push Notification', status: 'Sent', sourceIp: 'N/A', details: 'Successfully sent to 5,032 devices.', errorCode: null, errorMessage: null, failedPayload: null, troubleshooting: null },
|
||||||
|
{ id: '8', timestamp: new Date('2023-10-05T16:20:00Z'), action: 'Delivery Bounced', actor: 'System', channel: 'Email', status: 'Bounced', sourceIp: 'N/A', details: 'To: nonexist@example.com, Hard Bounce', errorCode: 'SMTP-550', errorMessage: 'User mailbox does not exist.', failedPayload: JSON.stringify({to: "nonexist@example.com", subject: "Your Weekly Digest"}), troubleshooting: 'Remove email from list. Verify email collection process.' },
|
||||||
|
|
||||||
|
// Add more dummy logs for pagination and filtering
|
||||||
|
...Array.from({ length: 42 }, (_, i) => ({
|
||||||
|
id: (i + 9).toString(),
|
||||||
|
timestamp: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
|
||||||
|
action: ['Notification Created', 'Notification Sent', 'Delivery Failed', 'Notification Opened', 'Template Updated', 'Notification Queued', 'Delivery Bounced'][Math.floor(Math.random() * 7)],
|
||||||
|
actor: ['Admin User', 'System', 'john.doe@example.com', 'jane.doe@example.com', 'support_agent', 'marketing_mgr'][Math.floor(Math.random() * 6)],
|
||||||
|
channel: channelOptions[Math.floor(Math.random() * channelOptions.length)],
|
||||||
|
status: statusOptions[Math.floor(Math.random() * statusOptions.length)],
|
||||||
|
sourceIp: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
|
||||||
|
details: `Log entry detail for event id ${i + 9}. This notification was processed with standard parameters.`,
|
||||||
|
errorCode: Math.random() > 0.85 ? `ERR-${Math.floor(Math.random() * 100) + 200}` : null,
|
||||||
|
errorMessage: Math.random() > 0.85 ? 'A simulated intermittent error occurred during processing.' : null,
|
||||||
|
failedPayload: Math.random() > 0.85 ? JSON.stringify({ data: "some example payload data", attempt: Math.floor(Math.random()*3)+1 }) : null,
|
||||||
|
troubleshooting: Math.random() > 0.85 ? 'Retry the operation. If persistent, check downstream service status.' : null,
|
||||||
|
}))
|
||||||
|
])
|
||||||
|
|
||||||
|
// This computed property will be passed to RsTable's `data` prop
|
||||||
|
const filteredLogs = computed(() => {
|
||||||
|
return dummyLogs.value.filter(log => {
|
||||||
|
const logDate = new Date(log.timestamp);
|
||||||
|
|
||||||
|
let dateMatch = true;
|
||||||
|
if (selectedDateRange.value && selectedDateRange.value.start && selectedDateRange.value.end) {
|
||||||
|
const startDate = new Date(selectedDateRange.value.start);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
const endDate = new Date(selectedDateRange.value.end);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
dateMatch = logDate >= startDate && logDate <= endDate;
|
||||||
|
} else if (selectedDateRange.value && selectedDateRange.value.start) {
|
||||||
|
const startDate = new Date(selectedDateRange.value.start);
|
||||||
|
startDate.setHours(0,0,0,0);
|
||||||
|
dateMatch = logDate >= startDate;
|
||||||
|
} else if (selectedDateRange.value && selectedDateRange.value.end) {
|
||||||
|
const endDate = new Date(selectedDateRange.value.end);
|
||||||
|
endDate.setHours(23,59,59,999);
|
||||||
|
dateMatch = logDate <= endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorMatch = !filters.value.actor || (log.actor && log.actor.toLowerCase().includes(filters.value.actor.toLowerCase()));
|
||||||
|
const channelMatch = !filters.value.channel || log.channel === filters.value.channel;
|
||||||
|
const statusMatch = !filters.value.status || log.status === filters.value.status;
|
||||||
|
const keywordMatch = !filters.value.keyword ||
|
||||||
|
(log.details && log.details.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
||||||
|
(log.action && log.action.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
||||||
|
(log.errorMessage && log.errorMessage.toLowerCase().includes(filters.value.keyword.toLowerCase()));
|
||||||
|
|
||||||
|
return dateMatch && actorMatch && channelMatch && statusMatch && keywordMatch;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fields for RsTable
|
||||||
|
const logTableFields = ['timestamp', 'action', 'actor', 'channel', 'status', 'actions']
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
console.log('Applying filters with values:', filters.value, selectedDateRange.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
filters.value = {
|
||||||
|
actor: '',
|
||||||
|
channel: null,
|
||||||
|
status: null,
|
||||||
|
keyword: ''
|
||||||
|
}
|
||||||
|
selectedDateRange.value = { start: null, end: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshLogs = () => {
|
||||||
|
console.log('Refreshing logs...')
|
||||||
|
// Simulate refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = (format) => {
|
||||||
|
console.log(`Exporting ${filteredLogs.value.length} logs to ${format}...`)
|
||||||
|
alert(`Exporting ${filteredLogs.value.length} log entries to ${format}. (Implementation pending)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Log Detail Modal ---
|
||||||
|
const isLogDetailModalOpen = ref(false)
|
||||||
|
const selectedLog = ref(null)
|
||||||
|
|
||||||
|
const viewLogDetails = (log) => {
|
||||||
|
selectedLog.value = log
|
||||||
|
isLogDetailModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary stats for quick overview
|
||||||
|
const summaryStats = computed(() => [
|
||||||
|
{
|
||||||
|
title: "Total Log Entries",
|
||||||
|
value: filteredLogs.value.length.toLocaleString(),
|
||||||
|
icon: "ic:outline-list-alt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Failed Deliveries",
|
||||||
|
value: filteredLogs.value.filter(log => log.status === 'Failed').length.toLocaleString(),
|
||||||
|
icon: "ic:outline-error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Successful Deliveries",
|
||||||
|
value: filteredLogs.value.filter(log => log.status === 'Sent').length.toLocaleString(),
|
||||||
|
icon: "ic:outline-check-circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Success Rate",
|
||||||
|
value: filteredLogs.value.length > 0 ?
|
||||||
|
Math.round((filteredLogs.value.filter(log => log.status === 'Sent').length / filteredLogs.value.length) * 100) + '%' : '0%',
|
||||||
|
icon: "ic:outline-trending-up"
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// Custom styles for FormKit consistency
|
||||||
|
:deep(.formkit-outer) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-input) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge component styles (if RsBadge doesn't exist, these can be adjusted)
|
||||||
|
.rs-badge {
|
||||||
|
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-success {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-danger {
|
||||||
|
@apply bg-red-100 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-warning {
|
||||||
|
@apply bg-yellow-100 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-info {
|
||||||
|
@apply bg-blue-100 text-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-secondary {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
</style>
|
743
pages/notification/log-audit/monitoring.vue
Normal file
743
pages/notification/log-audit/monitoring.vue
Normal file
@ -0,0 +1,743 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page Info Card -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-monitor"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Real-Time Monitoring</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Live monitoring of notification system performance with real-time alerts and system health indicators.
|
||||||
|
Track ongoing activities, monitor system load, and receive immediate notifications about issues.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- System Status Overview -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(status, index) in systemStatus"
|
||||||
|
: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-5 flex justify-center items-center rounded-2xl transition-all duration-300"
|
||||||
|
:class="status.status === 'healthy' ? 'bg-green-100' :
|
||||||
|
status.status === 'warning' ? 'bg-yellow-100' : 'bg-red-100'"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="text-3xl"
|
||||||
|
:class="status.status === 'healthy' ? 'text-green-600' :
|
||||||
|
status.status === 'warning' ? 'text-yellow-600' : 'text-red-600'"
|
||||||
|
:name="status.icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||||
|
{{ status.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ status.title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center mt-1">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full mr-2"
|
||||||
|
:class="status.status === 'healthy' ? 'bg-green-500' :
|
||||||
|
status.status === 'warning' ? 'bg-yellow-500' : 'bg-red-500'"
|
||||||
|
></div>
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium capitalize"
|
||||||
|
:class="status.status === 'healthy' ? 'text-green-600' :
|
||||||
|
status.status === 'warning' ? 'text-yellow-600' : 'text-red-600'"
|
||||||
|
>
|
||||||
|
{{ status.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Real-Time Controls -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #body>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Monitoring Controls</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full animate-pulse"
|
||||||
|
:class="isMonitoring ? 'bg-green-500' : 'bg-gray-400'"
|
||||||
|
></div>
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
{{ isMonitoring ? 'Live' : 'Paused' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="refreshInterval"
|
||||||
|
:options="refreshOptions"
|
||||||
|
outer-class="mb-0"
|
||||||
|
@input="updateRefreshInterval"
|
||||||
|
/>
|
||||||
|
<rs-button
|
||||||
|
:variant="isMonitoring ? 'danger-outline' : 'primary'"
|
||||||
|
size="sm"
|
||||||
|
@click="toggleMonitoring"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="isMonitoring ? 'ic:outline-pause' : 'ic:outline-play-arrow'"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
|
{{ isMonitoring ? 'Pause' : 'Start' }}
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="primary-outline" size="sm" @click="refreshData">
|
||||||
|
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- System Performance Dashboard -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-speed" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">System Performance</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="exportPerformanceData">
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1"/> Export Data
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
|
<!-- CPU Usage -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||||
|
<div class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-primary">{{ performanceMetrics.cpu }}%</div>
|
||||||
|
<div class="text-xs text-gray-600">CPU</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Memory Usage -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||||
|
<div class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-primary">{{ performanceMetrics.memory }}%</div>
|
||||||
|
<div class="text-xs text-gray-600">Memory</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue Load -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||||
|
<div class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-primary">{{ performanceMetrics.queueLoad }}%</div>
|
||||||
|
<div class="text-xs text-gray-600">Queue Load</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Performance Chart Placeholder -->
|
||||||
|
<div class="h-64 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="ic:outline-show-chart" class="text-4xl text-gray-400 mb-2"/>
|
||||||
|
<p class="text-gray-500">Real-time Performance Chart</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Implementation pending for live performance metrics</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Live Activity & Alerts Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Live Activity Feed -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-notifications-active" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Live Activity Feed</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-button variant="outline" size="sm" @click="clearActivityFeed">
|
||||||
|
<Icon name="ic:outline-clear" class="mr-1"/> Clear
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-96 overflow-y-auto space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(activity, index) in liveActivityFeed"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-start p-3 bg-gray-50 dark:bg-gray-800 rounded-lg transition-all duration-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3 mt-2 flex-shrink-0"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': activity.type === 'success',
|
||||||
|
'bg-blue-500': activity.type === 'info',
|
||||||
|
'bg-yellow-500': activity.type === 'warning',
|
||||||
|
'bg-red-500': activity.type === 'error',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-medium text-sm">{{ activity.action }}</p>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">{{ activity.details }}</p>
|
||||||
|
<div class="flex items-center mt-2 text-xs text-gray-500">
|
||||||
|
<Icon name="ic:outline-access-time" class="mr-1"/>
|
||||||
|
<span>{{ activity.timestamp }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span>{{ activity.source }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="liveActivityFeed.length === 0" class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-wifi-tethering" class="text-3xl mb-2 mx-auto"/>
|
||||||
|
<p>Waiting for live activity...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Error Alerts -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-warning" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Error Alerts</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-badge
|
||||||
|
:variant="errorAlerts.filter(a => a.severity === 'critical').length > 0 ? 'danger' : 'secondary'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ errorAlerts.length }} Active
|
||||||
|
</rs-badge>
|
||||||
|
<rs-button variant="outline" size="sm" @click="acknowledgeAllAlerts">
|
||||||
|
<Icon name="ic:outline-check" class="mr-1"/> Acknowledge All
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-96 overflow-y-auto space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(alert, index) in errorAlerts"
|
||||||
|
:key="index"
|
||||||
|
class="p-3 rounded-lg border-l-4 transition-all duration-300 hover:shadow-sm"
|
||||||
|
:class="{
|
||||||
|
'bg-red-50 border-red-400': alert.severity === 'critical',
|
||||||
|
'bg-yellow-50 border-yellow-400': alert.severity === 'warning',
|
||||||
|
'bg-blue-50 border-blue-400': alert.severity === 'info',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
:name="alert.severity === 'critical' ? 'ic:outline-error' :
|
||||||
|
alert.severity === 'warning' ? 'ic:outline-warning' : 'ic:outline-info'"
|
||||||
|
:class="{
|
||||||
|
'text-red-600': alert.severity === 'critical',
|
||||||
|
'text-yellow-600': alert.severity === 'warning',
|
||||||
|
'text-blue-600': alert.severity === 'info',
|
||||||
|
}"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-sm">{{ alert.title }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">{{ alert.description }}</p>
|
||||||
|
<div class="flex items-center mt-2 text-xs text-gray-500">
|
||||||
|
<span>{{ alert.timestamp }}</span>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<span>{{ alert.component }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="acknowledgeAlert(index)"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-check" />
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorAlerts.length === 0" class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-check-circle" class="text-3xl mb-2 mx-auto text-green-500"/>
|
||||||
|
<p>No active alerts</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">All systems operating normally</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue Status -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-queue" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Queue Status</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="navigateTo('/notification/queue-scheduler/monitor')">
|
||||||
|
View Queue Monitor
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="queue in queueStatus"
|
||||||
|
:key="queue.name"
|
||||||
|
class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="font-medium">{{ queue.name }}</span>
|
||||||
|
<rs-badge
|
||||||
|
:variant="queue.status === 'active' ? 'success' :
|
||||||
|
queue.status === 'warning' ? 'warning' : 'danger'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ queue.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-primary mb-1">{{ queue.count }}</div>
|
||||||
|
<div class="text-sm text-gray-600">{{ queue.description }}</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||||
|
<div
|
||||||
|
class="h-2 rounded-full transition-all duration-300"
|
||||||
|
:class="queue.status === 'active' ? 'bg-green-500' :
|
||||||
|
queue.status === 'warning' ? 'bg-yellow-500' : 'bg-red-500'"
|
||||||
|
:style="{ width: queue.utilization + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">{{ queue.utilization }}% utilized</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Recent Logs -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-history" class="mr-2 text-primary"/>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Activity Logs</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="navigateTo('/notification/log-audit/logs')">
|
||||||
|
View All Logs
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in recentLogs"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': log.status === 'sent' || log.status === 'created',
|
||||||
|
'bg-yellow-500': log.status === 'queued',
|
||||||
|
'bg-red-500': log.status === 'failed',
|
||||||
|
'bg-blue-500': log.status === 'opened',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ log.action }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ log.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium capitalize">{{ log.status }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ log.time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Real-Time Monitoring",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs & Audit Trail",
|
||||||
|
path: "/notification/log-audit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Monitoring",
|
||||||
|
path: "/notification/log-audit/monitoring",
|
||||||
|
type: "current"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
// Monitoring state
|
||||||
|
const isMonitoring = ref(true)
|
||||||
|
const refreshInterval = ref('5s')
|
||||||
|
const refreshIntervalId = ref(null)
|
||||||
|
|
||||||
|
const refreshOptions = [
|
||||||
|
{ label: '1 second', value: '1s' },
|
||||||
|
{ label: '5 seconds', value: '5s' },
|
||||||
|
{ label: '10 seconds', value: '10s' },
|
||||||
|
{ label: '30 seconds', value: '30s' },
|
||||||
|
{ label: '1 minute', value: '1m' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// System status data
|
||||||
|
const systemStatus = ref([
|
||||||
|
{
|
||||||
|
title: "System Health",
|
||||||
|
value: "Healthy",
|
||||||
|
icon: "ic:outline-favorite",
|
||||||
|
status: "healthy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Throughput",
|
||||||
|
value: "12,847/min",
|
||||||
|
icon: "ic:outline-speed",
|
||||||
|
status: "healthy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Error Rate",
|
||||||
|
value: "0.02%",
|
||||||
|
icon: "ic:outline-error-outline",
|
||||||
|
status: "healthy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Response Time",
|
||||||
|
value: "145ms",
|
||||||
|
icon: "ic:outline-timer",
|
||||||
|
status: "warning"
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Performance metrics
|
||||||
|
const performanceMetrics = ref({
|
||||||
|
cpu: 23,
|
||||||
|
memory: 67,
|
||||||
|
queueLoad: 45
|
||||||
|
})
|
||||||
|
|
||||||
|
// Live activity feed
|
||||||
|
const liveActivityFeed = ref([
|
||||||
|
{
|
||||||
|
action: "Email Batch Processed",
|
||||||
|
details: "Successfully sent 1,250 welcome emails to new users",
|
||||||
|
timestamp: "Just now",
|
||||||
|
source: "Email Service",
|
||||||
|
type: "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "SMS Delivery Completed",
|
||||||
|
details: "OTP messages delivered to 89 recipients",
|
||||||
|
timestamp: "2 seconds ago",
|
||||||
|
source: "SMS Gateway",
|
||||||
|
type: "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Push Notification Sent",
|
||||||
|
details: "Order confirmation sent to 456 devices",
|
||||||
|
timestamp: "8 seconds ago",
|
||||||
|
source: "Push Service",
|
||||||
|
type: "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Webhook Timeout",
|
||||||
|
details: "Webhook to api.example.com timed out after 30 seconds",
|
||||||
|
timestamp: "15 seconds ago",
|
||||||
|
source: "Webhook Service",
|
||||||
|
type: "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Rate Limit Triggered",
|
||||||
|
details: "SMS rate limit reached for provider Twilio",
|
||||||
|
timestamp: "32 seconds ago",
|
||||||
|
source: "Rate Limiter",
|
||||||
|
type: "warning"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Error alerts
|
||||||
|
const errorAlerts = ref([
|
||||||
|
{
|
||||||
|
title: "Database Connection Pool Exhausted",
|
||||||
|
description: "Connection pool has reached maximum capacity. New connections are being queued.",
|
||||||
|
timestamp: "2 minutes ago",
|
||||||
|
component: "Database",
|
||||||
|
severity: "critical"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Email Provider Rate Limit",
|
||||||
|
description: "SendGrid API rate limit reached. Email delivery is temporarily throttled.",
|
||||||
|
timestamp: "5 minutes ago",
|
||||||
|
component: "Email Service",
|
||||||
|
severity: "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "High Memory Usage",
|
||||||
|
description: "System memory usage has exceeded 85% threshold.",
|
||||||
|
timestamp: "8 minutes ago",
|
||||||
|
component: "System",
|
||||||
|
severity: "warning"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Queue status
|
||||||
|
const queueStatus = ref([
|
||||||
|
{
|
||||||
|
name: "Email Queue",
|
||||||
|
count: "1,247",
|
||||||
|
description: "Pending emails",
|
||||||
|
status: "active",
|
||||||
|
utilization: 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SMS Queue",
|
||||||
|
count: "89",
|
||||||
|
description: "Pending SMS",
|
||||||
|
status: "active",
|
||||||
|
utilization: 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Push Queue",
|
||||||
|
count: "3,456",
|
||||||
|
description: "Pending push notifications",
|
||||||
|
status: "warning",
|
||||||
|
utilization: 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Webhook Queue",
|
||||||
|
count: "12",
|
||||||
|
description: "Pending webhooks",
|
||||||
|
status: "active",
|
||||||
|
utilization: 8
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Recent logs
|
||||||
|
const recentLogs = ref([
|
||||||
|
{
|
||||||
|
action: "Email Campaign Sent",
|
||||||
|
description: "Newsletter campaign to 50k subscribers",
|
||||||
|
status: "sent",
|
||||||
|
time: "1 minute ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "SMS Batch Queued",
|
||||||
|
description: "OTP messages for authentication",
|
||||||
|
status: "queued",
|
||||||
|
time: "3 minutes ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Push Notification Failed",
|
||||||
|
description: "Failed to deliver to iOS devices",
|
||||||
|
status: "failed",
|
||||||
|
time: "5 minutes ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Email Opened",
|
||||||
|
description: "User opened promotional email",
|
||||||
|
status: "opened",
|
||||||
|
time: "7 minutes ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "Webhook Created",
|
||||||
|
description: "New webhook endpoint configured",
|
||||||
|
status: "created",
|
||||||
|
time: "10 minutes ago"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const toggleMonitoring = () => {
|
||||||
|
isMonitoring.value = !isMonitoring.value
|
||||||
|
if (isMonitoring.value) {
|
||||||
|
startMonitoring()
|
||||||
|
} else {
|
||||||
|
stopMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRefreshInterval = () => {
|
||||||
|
if (isMonitoring.value) {
|
||||||
|
stopMonitoring()
|
||||||
|
startMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMonitoring = () => {
|
||||||
|
const intervalMs = {
|
||||||
|
'1s': 1000,
|
||||||
|
'5s': 5000,
|
||||||
|
'10s': 10000,
|
||||||
|
'30s': 30000,
|
||||||
|
'1m': 60000
|
||||||
|
}[refreshInterval.value] || 5000
|
||||||
|
|
||||||
|
refreshIntervalId.value = setInterval(() => {
|
||||||
|
// Simulate real-time updates
|
||||||
|
simulateActivityUpdate()
|
||||||
|
updatePerformanceMetrics()
|
||||||
|
}, intervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopMonitoring = () => {
|
||||||
|
if (refreshIntervalId.value) {
|
||||||
|
clearInterval(refreshIntervalId.value)
|
||||||
|
refreshIntervalId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const simulateActivityUpdate = () => {
|
||||||
|
// Add a new activity to the feed
|
||||||
|
const activities = [
|
||||||
|
{ action: "Email Sent", details: "Marketing email delivered successfully", type: "success", source: "Email Service" },
|
||||||
|
{ action: "SMS Delivered", details: "OTP message sent to user", type: "success", source: "SMS Gateway" },
|
||||||
|
{ action: "Push Failed", details: "Device token expired", type: "error", source: "Push Service" },
|
||||||
|
{ action: "Webhook Called", details: "Order confirmation webhook triggered", type: "info", source: "Webhook Service" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const newActivity = {
|
||||||
|
...activities[Math.floor(Math.random() * activities.length)],
|
||||||
|
timestamp: "Just now"
|
||||||
|
}
|
||||||
|
|
||||||
|
liveActivityFeed.value.unshift(newActivity)
|
||||||
|
if (liveActivityFeed.value.length > 10) {
|
||||||
|
liveActivityFeed.value.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePerformanceMetrics = () => {
|
||||||
|
// Simulate metric updates
|
||||||
|
performanceMetrics.value.cpu = Math.max(10, Math.min(90, performanceMetrics.value.cpu + (Math.random() - 0.5) * 10))
|
||||||
|
performanceMetrics.value.memory = Math.max(20, Math.min(95, performanceMetrics.value.memory + (Math.random() - 0.5) * 5))
|
||||||
|
performanceMetrics.value.queueLoad = Math.max(0, Math.min(100, performanceMetrics.value.queueLoad + (Math.random() - 0.5) * 15))
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshData = () => {
|
||||||
|
console.log('Refreshing monitoring data...')
|
||||||
|
simulateActivityUpdate()
|
||||||
|
updatePerformanceMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearActivityFeed = () => {
|
||||||
|
liveActivityFeed.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const acknowledgeAlert = (index) => {
|
||||||
|
errorAlerts.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const acknowledgeAllAlerts = () => {
|
||||||
|
errorAlerts.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportPerformanceData = () => {
|
||||||
|
console.log('Exporting performance data...')
|
||||||
|
alert('Exporting performance data. (Implementation pending)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
if (isMonitoring.value) {
|
||||||
|
startMonitoring()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopMonitoring()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// Custom styles for FormKit consistency
|
||||||
|
:deep(.formkit-outer) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-input) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge component styles (if RsBadge doesn't exist, these can be adjusted)
|
||||||
|
.rs-badge {
|
||||||
|
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-success {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-danger {
|
||||||
|
@apply bg-red-100 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-warning {
|
||||||
|
@apply bg-yellow-100 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-info {
|
||||||
|
@apply bg-blue-100 text-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-secondary {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
</style>
|
834
pages/notification/log-audit/reports.vue
Normal file
834
pages/notification/log-audit/reports.vue
Normal file
@ -0,0 +1,834 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Page Info Card -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-file-download"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Reports & Export</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Generate comprehensive reports and export log data for compliance, auditing, and
|
||||||
|
analysis purposes. Create custom reports, schedule automated exports, and ensure
|
||||||
|
regulatory compliance.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Quick Export Actions -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(action, index) in quickExportActions"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg cursor-pointer"
|
||||||
|
@click="quickExport(action.type)"
|
||||||
|
>
|
||||||
|
<div class="pt-5 pb-3 px-5 text-center">
|
||||||
|
<div
|
||||||
|
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30 mx-auto mb-4 w-fit"
|
||||||
|
>
|
||||||
|
<Icon class="text-primary text-3xl" :name="action.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<span class="block font-bold text-lg leading-tight text-primary mb-2">
|
||||||
|
{{ action.title }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-600">
|
||||||
|
{{ action.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Report Builder -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-build" class="mr-2 text-primary" />
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Custom Report Builder</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- Report Configuration -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Report Name"
|
||||||
|
v-model="customReport.name"
|
||||||
|
placeholder="Enter report name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Report Type"
|
||||||
|
v-model="customReport.type"
|
||||||
|
:options="reportTypeOptions"
|
||||||
|
placeholder="Select report type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Date Range"
|
||||||
|
v-model="customReport.dateRange"
|
||||||
|
:options="dateRangeOptions"
|
||||||
|
placeholder="Select date range"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Export Format"
|
||||||
|
v-model="customReport.format"
|
||||||
|
:options="exportFormatOptions"
|
||||||
|
placeholder="Select format"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Include Channels"
|
||||||
|
v-model="customReport.channels"
|
||||||
|
:options="channelOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Include Status"
|
||||||
|
v-model="customReport.statuses"
|
||||||
|
:options="statusOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Preview -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h4 class="text-lg font-semibold">Report Preview</h4>
|
||||||
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg min-h-64">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-medium">Report Name:</span>
|
||||||
|
<span class="text-primary">{{
|
||||||
|
customReport.name || "Untitled Report"
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-medium">Type:</span>
|
||||||
|
<span>{{ getReportTypeLabel(customReport.type) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-medium">Date Range:</span>
|
||||||
|
<span>{{ getDateRangeLabel(customReport.dateRange) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-medium">Format:</span>
|
||||||
|
<span>{{ getFormatLabel(customReport.format) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<span class="font-medium">Channels:</span>
|
||||||
|
<div class="text-right">
|
||||||
|
<div v-if="customReport.channels.length === 0" class="text-gray-500">
|
||||||
|
All channels
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="channel in customReport.channels"
|
||||||
|
:key="channel"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
{{ channel }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<span class="font-medium">Status:</span>
|
||||||
|
<div class="text-right">
|
||||||
|
<div v-if="customReport.statuses.length === 0" class="text-gray-500">
|
||||||
|
All statuses
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="status in customReport.statuses"
|
||||||
|
:key="status"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<rs-button
|
||||||
|
@click="generateCustomReport"
|
||||||
|
variant="primary"
|
||||||
|
:disabled="!customReport.name || !customReport.type"
|
||||||
|
class="flex-1"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-play-arrow" class="mr-1" /> Generate Report
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="saveReportTemplate" variant="secondary-outline">
|
||||||
|
<Icon name="ic:outline-save" class="mr-1" /> Save Template
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Compliance Reports -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-verified-user" class="mr-2 text-primary" />
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Compliance Reports</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(compliance, index) in complianceReports"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon :name="compliance.icon" class="mr-2 text-primary" />
|
||||||
|
<h4 class="font-semibold">{{ compliance.title }}</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">{{ compliance.description }}</p>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div
|
||||||
|
v-for="(feature, idx) in compliance.features"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-check" class="mr-2 text-green-500 text-xs" />
|
||||||
|
<span>{{ feature }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span>Last Generated:</span>
|
||||||
|
<span class="font-medium">{{ compliance.lastGenerated }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<rs-button
|
||||||
|
@click="generateComplianceReport(compliance.type)"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-file-download" class="mr-1" /> Generate Report
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Scheduled Reports -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-schedule" class="mr-2 text-primary" />
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Scheduled Reports</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="primary" size="sm" @click="openScheduleModal">
|
||||||
|
<Icon name="ic:outline-add" class="mr-1" /> Schedule Report
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<rs-table
|
||||||
|
v-if="scheduledReports.length > 0"
|
||||||
|
:data="scheduledReports"
|
||||||
|
:field="scheduledReportFields"
|
||||||
|
:options="{
|
||||||
|
variant: 'default',
|
||||||
|
striped: true,
|
||||||
|
borderless: false,
|
||||||
|
hover: true,
|
||||||
|
}"
|
||||||
|
:options-advanced="{
|
||||||
|
sortable: true,
|
||||||
|
responsive: true,
|
||||||
|
outsideBorder: true,
|
||||||
|
}"
|
||||||
|
advanced
|
||||||
|
>
|
||||||
|
<template #status="{ value }">
|
||||||
|
<rs-badge
|
||||||
|
:variant="
|
||||||
|
value.status === 'Active'
|
||||||
|
? 'success'
|
||||||
|
: value.status === 'Paused'
|
||||||
|
? 'warning'
|
||||||
|
: 'secondary'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ value.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ value }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<rs-button
|
||||||
|
variant="primary-text"
|
||||||
|
size="sm"
|
||||||
|
@click="editScheduledReport(value)"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-edit" class="mr-1" /> Edit
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
variant="danger-text"
|
||||||
|
size="sm"
|
||||||
|
@click="deleteScheduledReport(value)"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-delete" class="mr-1" /> Delete
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-table>
|
||||||
|
|
||||||
|
<div v-else class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-schedule" class="text-4xl mb-2 mx-auto" />
|
||||||
|
<p>No scheduled reports configured</p>
|
||||||
|
<rs-button variant="primary-outline" @click="openScheduleModal" class="mt-4">
|
||||||
|
<Icon name="ic:outline-add" class="mr-1" /> Schedule Your First Report
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Export History -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="ic:outline-history" class="mr-2 text-primary" />
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Export History</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="clearExportHistory">
|
||||||
|
<Icon name="ic:outline-clear" class="mr-1" /> Clear History
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(export_, index) in exportHistory"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
:name="
|
||||||
|
export_.format === 'pdf'
|
||||||
|
? 'ic:outline-picture-as-pdf'
|
||||||
|
: export_.format === 'csv'
|
||||||
|
? 'ic:outline-table-chart'
|
||||||
|
: 'ic:outline-grid-on'
|
||||||
|
"
|
||||||
|
class="mr-3 text-primary text-xl"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ export_.name }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ export_.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium">{{ export_.size }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ export_.timestamp }}</p>
|
||||||
|
</div>
|
||||||
|
<rs-badge
|
||||||
|
:variant="
|
||||||
|
export_.status === 'completed'
|
||||||
|
? 'success'
|
||||||
|
: export_.status === 'processing'
|
||||||
|
? 'warning'
|
||||||
|
: 'danger'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ export_.status }}
|
||||||
|
</rs-badge>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<rs-button
|
||||||
|
v-if="export_.status === 'completed'"
|
||||||
|
variant="primary-text"
|
||||||
|
size="sm"
|
||||||
|
@click="downloadExport(export_)"
|
||||||
|
>
|
||||||
|
<Icon name="ic:outline-download" />
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="danger-text" size="sm" @click="deleteExport(index)">
|
||||||
|
<Icon name="ic:outline-delete" />
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="exportHistory.length === 0" class="text-center py-8 text-gray-500">
|
||||||
|
<Icon name="ic:outline-folder-open" class="text-4xl mb-2 mx-auto" />
|
||||||
|
<p>No export history available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Schedule Report Modal -->
|
||||||
|
<rs-modal
|
||||||
|
v-model="isScheduleModalOpen"
|
||||||
|
title="Schedule Report"
|
||||||
|
size="lg"
|
||||||
|
:overlay-close="true"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Report Name"
|
||||||
|
v-model="scheduleForm.name"
|
||||||
|
placeholder="Enter report name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Report Type"
|
||||||
|
v-model="scheduleForm.type"
|
||||||
|
:options="reportTypeOptions"
|
||||||
|
placeholder="Select report type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Frequency"
|
||||||
|
v-model="scheduleForm.frequency"
|
||||||
|
:options="frequencyOptions"
|
||||||
|
placeholder="Select frequency"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Export Format"
|
||||||
|
v-model="scheduleForm.format"
|
||||||
|
:options="exportFormatOptions"
|
||||||
|
placeholder="Select format"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="email"
|
||||||
|
label="Email Recipients"
|
||||||
|
v-model="scheduleForm.recipients"
|
||||||
|
placeholder="Enter email addresses (comma separated)"
|
||||||
|
help="Enter multiple email addresses separated by commas"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Include in export history"
|
||||||
|
v-model="scheduleForm.includeInHistory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<rs-button @click="isScheduleModalOpen = false" variant="secondary-outline">
|
||||||
|
Cancel
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="createScheduledReport" variant="primary">
|
||||||
|
<Icon name="ic:outline-schedule" class="mr-1" /> Schedule Report
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Reports & Export",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs & Audit Trail",
|
||||||
|
path: "/notification/log-audit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reports",
|
||||||
|
path: "/notification/log-audit/reports",
|
||||||
|
type: "current",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// Quick export actions
|
||||||
|
const quickExportActions = ref([
|
||||||
|
{
|
||||||
|
title: "CSV Export",
|
||||||
|
description: "Export current data to CSV format",
|
||||||
|
icon: "ic:outline-table-chart",
|
||||||
|
type: "csv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PDF Report",
|
||||||
|
description: "Generate comprehensive PDF report",
|
||||||
|
icon: "ic:outline-picture-as-pdf",
|
||||||
|
type: "pdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Excel Export",
|
||||||
|
description: "Export data to Excel spreadsheet",
|
||||||
|
icon: "ic:outline-grid-on",
|
||||||
|
type: "excel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "JSON Export",
|
||||||
|
description: "Export raw data in JSON format",
|
||||||
|
icon: "ic:outline-code",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Custom report builder
|
||||||
|
const customReport = ref({
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
dateRange: "",
|
||||||
|
format: "",
|
||||||
|
channels: [],
|
||||||
|
statuses: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const reportTypeOptions = [
|
||||||
|
{ label: "Delivery Report", value: "delivery" },
|
||||||
|
{ label: "Performance Analytics", value: "performance" },
|
||||||
|
{ label: "Error Analysis", value: "errors" },
|
||||||
|
{ label: "User Engagement", value: "engagement" },
|
||||||
|
{ label: "Channel Comparison", value: "channels" },
|
||||||
|
{ label: "Audit Trail", value: "audit" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const dateRangeOptions = [
|
||||||
|
{ label: "Last 24 Hours", value: "1d" },
|
||||||
|
{ label: "Last 7 Days", value: "7d" },
|
||||||
|
{ label: "Last 30 Days", value: "30d" },
|
||||||
|
{ label: "Last 90 Days", value: "90d" },
|
||||||
|
{ label: "Last 12 Months", value: "12m" },
|
||||||
|
{ label: "Custom Range", value: "custom" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const exportFormatOptions = [
|
||||||
|
{ label: "CSV", value: "csv" },
|
||||||
|
{ label: "PDF", value: "pdf" },
|
||||||
|
{ label: "Excel", value: "excel" },
|
||||||
|
{ label: "JSON", value: "json" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const channelOptions = [
|
||||||
|
{ label: "Email", value: "Email" },
|
||||||
|
{ label: "SMS", value: "SMS" },
|
||||||
|
{ label: "Push Notification", value: "Push Notification" },
|
||||||
|
{ label: "Webhook", value: "Webhook" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "Sent", value: "Sent" },
|
||||||
|
{ label: "Failed", value: "Failed" },
|
||||||
|
{ label: "Bounced", value: "Bounced" },
|
||||||
|
{ label: "Opened", value: "Opened" },
|
||||||
|
{ label: "Queued", value: "Queued" },
|
||||||
|
{ label: "Created", value: "Created" },
|
||||||
|
{ label: "Updated", value: "Updated" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Compliance reports
|
||||||
|
const complianceReports = ref([
|
||||||
|
{
|
||||||
|
title: "GDPR Compliance",
|
||||||
|
description:
|
||||||
|
"General Data Protection Regulation compliance report including data processing activities and user consent tracking.",
|
||||||
|
icon: "ic:outline-security",
|
||||||
|
type: "gdpr",
|
||||||
|
lastGenerated: "2 days ago",
|
||||||
|
features: [
|
||||||
|
"Data processing logs",
|
||||||
|
"User consent tracking",
|
||||||
|
"Data deletion records",
|
||||||
|
"Cross-border transfers",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SOC2 Audit",
|
||||||
|
description:
|
||||||
|
"Service Organization Control 2 audit report covering security, availability, and processing integrity.",
|
||||||
|
icon: "ic:outline-shield",
|
||||||
|
type: "soc2",
|
||||||
|
lastGenerated: "1 week ago",
|
||||||
|
features: [
|
||||||
|
"Security controls assessment",
|
||||||
|
"Availability monitoring",
|
||||||
|
"Processing integrity",
|
||||||
|
"Confidentiality measures",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "HIPAA Compliance",
|
||||||
|
description:
|
||||||
|
"Health Insurance Portability and Accountability Act compliance report for healthcare-related notifications.",
|
||||||
|
icon: "ic:outline-local-hospital",
|
||||||
|
type: "hipaa",
|
||||||
|
lastGenerated: "3 days ago",
|
||||||
|
features: [
|
||||||
|
"PHI access logs",
|
||||||
|
"Encryption compliance",
|
||||||
|
"Audit trail integrity",
|
||||||
|
"Risk assessment",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Scheduled reports
|
||||||
|
const isScheduleModalOpen = ref(false);
|
||||||
|
const scheduleForm = ref({
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
frequency: "",
|
||||||
|
format: "",
|
||||||
|
recipients: "",
|
||||||
|
includeInHistory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const frequencyOptions = [
|
||||||
|
{ label: "Daily", value: "daily" },
|
||||||
|
{ label: "Weekly", value: "weekly" },
|
||||||
|
{ label: "Monthly", value: "monthly" },
|
||||||
|
{ label: "Quarterly", value: "quarterly" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const scheduledReports = ref([
|
||||||
|
{
|
||||||
|
name: "Daily Delivery Report",
|
||||||
|
type: "Delivery Analytics",
|
||||||
|
frequency: "Daily",
|
||||||
|
format: "PDF",
|
||||||
|
recipients: "admin@company.com",
|
||||||
|
status: "Active",
|
||||||
|
nextRun: "Tomorrow 9:00 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weekly Performance Summary",
|
||||||
|
type: "Performance Analytics",
|
||||||
|
frequency: "Weekly",
|
||||||
|
format: "Excel",
|
||||||
|
recipients: "team@company.com",
|
||||||
|
status: "Active",
|
||||||
|
nextRun: "Monday 8:00 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Monthly Compliance Report",
|
||||||
|
type: "Audit Trail",
|
||||||
|
frequency: "Monthly",
|
||||||
|
format: "PDF",
|
||||||
|
recipients: "compliance@company.com",
|
||||||
|
status: "Paused",
|
||||||
|
nextRun: "1st of next month",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const scheduledReportFields = [
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"frequency",
|
||||||
|
"format",
|
||||||
|
"status",
|
||||||
|
"nextRun",
|
||||||
|
"actions",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Export history
|
||||||
|
const exportHistory = ref([
|
||||||
|
{
|
||||||
|
name: "Notification Analytics Report",
|
||||||
|
description: "Monthly performance analysis",
|
||||||
|
format: "pdf",
|
||||||
|
size: "2.4 MB",
|
||||||
|
timestamp: "2 hours ago",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delivery Logs Export",
|
||||||
|
description: "Last 30 days delivery data",
|
||||||
|
format: "csv",
|
||||||
|
size: "856 KB",
|
||||||
|
timestamp: "1 day ago",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GDPR Compliance Report",
|
||||||
|
description: "Quarterly compliance audit",
|
||||||
|
format: "pdf",
|
||||||
|
size: "1.2 MB",
|
||||||
|
timestamp: "3 days ago",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error Analysis Report",
|
||||||
|
description: "Failed notification analysis",
|
||||||
|
format: "excel",
|
||||||
|
size: "1.8 MB",
|
||||||
|
timestamp: "5 days ago",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getReportTypeLabel = (value) => {
|
||||||
|
const option = reportTypeOptions.find((opt) => opt.value === value);
|
||||||
|
return option ? option.label : "Not selected";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateRangeLabel = (value) => {
|
||||||
|
const option = dateRangeOptions.find((opt) => opt.value === value);
|
||||||
|
return option ? option.label : "Not selected";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFormatLabel = (value) => {
|
||||||
|
const option = exportFormatOptions.find((opt) => opt.value === value);
|
||||||
|
return option ? option.label : "Not selected";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const quickExport = (type) => {
|
||||||
|
console.log(`Quick export: ${type}`);
|
||||||
|
alert(`Exporting data in ${type} format. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateCustomReport = () => {
|
||||||
|
console.log("Generating custom report:", customReport.value);
|
||||||
|
alert(`Generating custom report: ${customReport.value.name}. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveReportTemplate = () => {
|
||||||
|
console.log("Saving report template:", customReport.value);
|
||||||
|
alert(`Saving report template: ${customReport.value.name}. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateComplianceReport = (type) => {
|
||||||
|
console.log(`Generating ${type} compliance report`);
|
||||||
|
alert(`Generating ${type} compliance report. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openScheduleModal = () => {
|
||||||
|
scheduleForm.value = {
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
frequency: "",
|
||||||
|
format: "",
|
||||||
|
recipients: "",
|
||||||
|
includeInHistory: true,
|
||||||
|
};
|
||||||
|
isScheduleModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScheduledReport = () => {
|
||||||
|
console.log("Creating scheduled report:", scheduleForm.value);
|
||||||
|
scheduledReports.value.push({
|
||||||
|
...scheduleForm.value,
|
||||||
|
status: "Active",
|
||||||
|
nextRun: "Tomorrow 9:00 AM",
|
||||||
|
});
|
||||||
|
isScheduleModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editScheduledReport = (report) => {
|
||||||
|
console.log("Editing scheduled report:", report);
|
||||||
|
alert(`Editing scheduled report: ${report.name}. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteScheduledReport = (report) => {
|
||||||
|
const index = scheduledReports.value.findIndex((r) => r.name === report.name);
|
||||||
|
if (index !== -1) {
|
||||||
|
scheduledReports.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearExportHistory = () => {
|
||||||
|
exportHistory.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadExport = (export_) => {
|
||||||
|
console.log("Downloading export:", export_);
|
||||||
|
alert(`Downloading ${export_.name}. (Implementation pending)`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteExport = (index) => {
|
||||||
|
exportHistory.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// Custom styles for FormKit consistency
|
||||||
|
:deep(.formkit-outer) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgb(107 114 128);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.formkit-input) {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge component styles (if RsBadge doesn't exist, these can be adjusted)
|
||||||
|
.rs-badge {
|
||||||
|
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-success {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-danger {
|
||||||
|
@apply bg-red-100 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-warning {
|
||||||
|
@apply bg-yellow-100 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-info {
|
||||||
|
@apply bg-blue-100 text-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rs-badge.variant-secondary {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
</style>
|
@ -155,24 +155,24 @@ definePageMeta({
|
|||||||
// Quick stats data
|
// Quick stats data
|
||||||
const quickStats = ref([
|
const quickStats = ref([
|
||||||
{
|
{
|
||||||
title: "Active Jobs",
|
title: "Throughput",
|
||||||
value: "1,247",
|
value: "12,847/min",
|
||||||
icon: "ic:outline-work",
|
icon: "ic:outline-speed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Completed Today",
|
title: "System Uptime",
|
||||||
value: "8,932",
|
value: "99.97%",
|
||||||
icon: "ic:outline-check-circle",
|
icon: "ic:outline-timeline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Failed Jobs",
|
title: "Active Workers",
|
||||||
value: "23",
|
value: "8",
|
||||||
icon: "ic:outline-error",
|
icon: "ic:outline-memory",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Avg Processing Time",
|
title: "Queue Load",
|
||||||
value: "2.3s",
|
value: "67%",
|
||||||
icon: "ic:outline-timer",
|
icon: "ic:outline-queue",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -190,6 +190,30 @@ const features = ref([
|
|||||||
"Filter by job type, channel, and status",
|
"Filter by job type, channel, and status",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Performance Monitoring",
|
||||||
|
description:
|
||||||
|
"Track system performance metrics including throughput, uptime, and scaling status to ensure 10,000+ notifications per minute.",
|
||||||
|
icon: "ic:outline-speed",
|
||||||
|
path: "/notification/queue-scheduler/performance",
|
||||||
|
useCases: [
|
||||||
|
"Monitor 10,000/min throughput requirement",
|
||||||
|
"Track 99.9% uptime compliance",
|
||||||
|
"Horizontal scaling status and configuration",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Priority Management",
|
||||||
|
description:
|
||||||
|
"Configure and manage different priority levels for notifications to ensure critical messages are processed first.",
|
||||||
|
icon: "ic:outline-priority-high",
|
||||||
|
path: "/notification/queue-scheduler/priority",
|
||||||
|
useCases: [
|
||||||
|
"Define critical, high, medium, low priority levels",
|
||||||
|
"Configure processing weights and order",
|
||||||
|
"Monitor priority queue performance",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Batch Processing",
|
title: "Batch Processing",
|
||||||
description:
|
description:
|
||||||
@ -214,6 +238,18 @@ const features = ref([
|
|||||||
"Debug with detailed error reasons",
|
"Debug with detailed error reasons",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Queue Persistence",
|
||||||
|
description:
|
||||||
|
"Configure queue data persistence to ensure notifications survive system restarts and failures.",
|
||||||
|
icon: "ic:outline-storage",
|
||||||
|
path: "/notification/queue-scheduler/persistence",
|
||||||
|
useCases: [
|
||||||
|
"Automatic backup and recovery",
|
||||||
|
"System restart data recovery",
|
||||||
|
"Storage configuration and monitoring",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Timezone Handling",
|
title: "Timezone Handling",
|
||||||
description:
|
description:
|
||||||
|
619
pages/notification/queue-scheduler/performance.vue
Normal file
619
pages/notification/queue-scheduler/performance.vue
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
<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-speed"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Performance Monitoring</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">Real-time Metrics</span>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshMetrics">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Monitor system performance metrics including throughput, uptime, and scaling status to ensure
|
||||||
|
the system meets the 10,000 notifications per minute requirement with 99.9% uptime.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Key Performance Indicators -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(kpi, index) in keyMetrics"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg"
|
||||||
|
:class="kpi.status === 'critical' ? 'border-red-500' : kpi.status === 'warning' ? 'border-yellow-500' : 'border-green-500'"
|
||||||
|
>
|
||||||
|
<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="kpi.bgColor"
|
||||||
|
>
|
||||||
|
<Icon class="text-2xl" :class="kpi.iconColor" :name="kpi.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight" :class="kpi.valueColor">
|
||||||
|
{{ kpi.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ kpi.title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center mt-1">
|
||||||
|
<Icon
|
||||||
|
class="text-xs mr-1"
|
||||||
|
:class="kpi.trend === 'up' ? 'text-green-500' : kpi.trend === 'down' ? 'text-red-500' : 'text-gray-500'"
|
||||||
|
:name="kpi.trend === 'up' ? 'ic:outline-trending-up' : kpi.trend === 'down' ? 'ic:outline-trending-down' : 'ic:outline-trending-flat'"
|
||||||
|
></Icon>
|
||||||
|
<span class="text-xs" :class="kpi.trend === 'up' ? 'text-green-500' : kpi.trend === 'down' ? 'text-red-500' : 'text-gray-500'">
|
||||||
|
{{ kpi.change }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Performance Charts -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Throughput Chart -->
|
||||||
|
<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-show-chart"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Throughput (Last 24 Hours)</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">Target: 10,000/min</span>
|
||||||
|
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon class="text-4xl text-gray-400 mb-2" name="ic:outline-show-chart"></Icon>
|
||||||
|
<p class="text-gray-600">Throughput Chart</p>
|
||||||
|
<p class="text-sm text-gray-500">Real-time notifications per minute</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid grid-cols-3 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">Current</p>
|
||||||
|
<p class="font-bold text-lg text-green-600">{{ currentThroughput }}/min</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">Peak Today</p>
|
||||||
|
<p class="font-bold text-lg text-blue-600">{{ peakThroughput }}/min</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-600">Average</p>
|
||||||
|
<p class="font-bold text-lg text-purple-600">{{ avgThroughput }}/min</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- System Uptime -->
|
||||||
|
<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-timeline"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">System Uptime</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">Target: 99.9%</span>
|
||||||
|
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Current Uptime -->
|
||||||
|
<div class="text-center p-6 bg-green-50 rounded-lg">
|
||||||
|
<div class="text-4xl font-bold text-green-600 mb-2">{{ currentUptime }}%</div>
|
||||||
|
<p class="text-green-700 font-medium">Current Uptime</p>
|
||||||
|
<p class="text-sm text-green-600 mt-1">{{ uptimeDuration }} continuous operation</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime History -->
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Last 24h</p>
|
||||||
|
<p class="font-bold text-lg">{{ uptime24h }}%</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Last 7 days</p>
|
||||||
|
<p class="font-bold text-lg">{{ uptime7d }}%</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Last 30 days</p>
|
||||||
|
<p class="font-bold text-lg">{{ uptime30d }}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Incidents -->
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-2">Recent Incidents</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(incident, index) in recentIncidents"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-2 bg-gray-50 rounded"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
|
||||||
|
<span class="text-sm">{{ incident.description }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-500">{{ incident.duration }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Worker Scaling 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-dns"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Horizontal Scaling Status</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="showScalingModal = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
||||||
|
Configure
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Active Workers -->
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<Icon class="text-3xl text-blue-600 mb-2" name="ic:outline-memory"></Icon>
|
||||||
|
<div class="text-2xl font-bold text-blue-600">{{ activeWorkers }}</div>
|
||||||
|
<p class="text-blue-700 font-medium">Active Workers</p>
|
||||||
|
<p class="text-sm text-blue-600 mt-1">Processing notifications</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-scaling Status -->
|
||||||
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<Icon class="text-3xl text-green-600 mb-2" name="ic:outline-auto-awesome"></Icon>
|
||||||
|
<div class="text-lg font-bold text-green-600">{{ autoScalingEnabled ? 'Enabled' : 'Disabled' }}</div>
|
||||||
|
<p class="text-green-700 font-medium">Auto-scaling</p>
|
||||||
|
<p class="text-sm text-green-600 mt-1">{{ autoScalingEnabled ? 'Automatically adjusting' : 'Manual scaling only' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue Load -->
|
||||||
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<Icon class="text-3xl text-purple-600 mb-2" name="ic:outline-queue"></Icon>
|
||||||
|
<div class="text-2xl font-bold text-purple-600">{{ queueLoad }}%</div>
|
||||||
|
<p class="text-purple-700 font-medium">Queue Load</p>
|
||||||
|
<p class="text-sm text-purple-600 mt-1">Current capacity usage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Worker Details -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<h4 class="font-medium text-gray-700 mb-4">Worker Instances</h4>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Worker ID
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
CPU Usage
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Memory Usage
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Jobs Processed
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Last Heartbeat
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr v-for="(worker, index) in workers" :key="index">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{{ worker.id }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 text-green-800': worker.status === 'healthy',
|
||||||
|
'bg-yellow-100 text-yellow-800': worker.status === 'warning',
|
||||||
|
'bg-red-100 text-red-800': worker.status === 'error'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ worker.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{{ worker.cpuUsage }}%
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{{ worker.memoryUsage }}%
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{{ worker.jobsProcessed.toLocaleString() }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{{ worker.lastHeartbeat }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Performance Alerts -->
|
||||||
|
<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-warning"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Performance Alerts</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="showAlertsConfig = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
||||||
|
Configure Alerts
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(alert, index) in performanceAlerts"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-4 rounded-lg"
|
||||||
|
:class="{
|
||||||
|
'bg-red-50 border border-red-200': alert.severity === 'critical',
|
||||||
|
'bg-yellow-50 border border-yellow-200': alert.severity === 'warning',
|
||||||
|
'bg-blue-50 border border-blue-200': alert.severity === 'info'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
class="mr-3 text-xl"
|
||||||
|
:class="{
|
||||||
|
'text-red-500': alert.severity === 'critical',
|
||||||
|
'text-yellow-500': alert.severity === 'warning',
|
||||||
|
'text-blue-500': alert.severity === 'info'
|
||||||
|
}"
|
||||||
|
:name="alert.icon"
|
||||||
|
></Icon>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium" :class="{
|
||||||
|
'text-red-800': alert.severity === 'critical',
|
||||||
|
'text-yellow-800': alert.severity === 'warning',
|
||||||
|
'text-blue-800': alert.severity === 'info'
|
||||||
|
}">
|
||||||
|
{{ alert.title }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm" :class="{
|
||||||
|
'text-red-600': alert.severity === 'critical',
|
||||||
|
'text-yellow-600': alert.severity === 'warning',
|
||||||
|
'text-blue-600': alert.severity === 'info'
|
||||||
|
}">
|
||||||
|
{{ alert.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-xs text-gray-500">{{ alert.timestamp }}</p>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="acknowledgeAlert(index)"
|
||||||
|
>
|
||||||
|
Acknowledge
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Scaling Configuration Modal -->
|
||||||
|
<rs-modal v-model="showScalingModal" title="Horizontal Scaling Configuration">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Auto-scaling</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="scalingConfig.autoScalingEnabled"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-600">Enable automatic worker scaling</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Min Workers</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="scalingConfig.minWorkers"
|
||||||
|
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 Workers</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="scalingConfig.maxWorkers"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Scale Up Threshold (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="scalingConfig.scaleUpThreshold"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Scale up when queue load exceeds this percentage</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Scale Down Threshold (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="scalingConfig.scaleDownThreshold"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Scale down when queue load falls below this percentage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<rs-button variant="outline" @click="showScalingModal = false">
|
||||||
|
Cancel
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="saveScalingConfig">
|
||||||
|
Save Configuration
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Performance Monitoring",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Performance",
|
||||||
|
path: "/notification/queue-scheduler/performance",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const showScalingModal = ref(false);
|
||||||
|
const showAlertsConfig = ref(false);
|
||||||
|
|
||||||
|
// Key Performance Metrics
|
||||||
|
const keyMetrics = ref([
|
||||||
|
{
|
||||||
|
title: "Throughput",
|
||||||
|
value: "12,847/min",
|
||||||
|
icon: "ic:outline-speed",
|
||||||
|
bgColor: "bg-green-100",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
valueColor: "text-green-600",
|
||||||
|
status: "healthy",
|
||||||
|
trend: "up",
|
||||||
|
change: "+15.3%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "System Uptime",
|
||||||
|
value: "99.97%",
|
||||||
|
icon: "ic:outline-timeline",
|
||||||
|
bgColor: "bg-blue-100",
|
||||||
|
iconColor: "text-blue-600",
|
||||||
|
valueColor: "text-blue-600",
|
||||||
|
status: "healthy",
|
||||||
|
trend: "stable",
|
||||||
|
change: "0.02%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Active Workers",
|
||||||
|
value: "8",
|
||||||
|
icon: "ic:outline-memory",
|
||||||
|
bgColor: "bg-purple-100",
|
||||||
|
iconColor: "text-purple-600",
|
||||||
|
valueColor: "text-purple-600",
|
||||||
|
status: "healthy",
|
||||||
|
trend: "up",
|
||||||
|
change: "+2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Queue Load",
|
||||||
|
value: "67%",
|
||||||
|
icon: "ic:outline-queue",
|
||||||
|
bgColor: "bg-yellow-100",
|
||||||
|
iconColor: "text-yellow-600",
|
||||||
|
valueColor: "text-yellow-600",
|
||||||
|
status: "warning",
|
||||||
|
trend: "up",
|
||||||
|
change: "+12%"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Throughput metrics
|
||||||
|
const currentThroughput = ref("12,847");
|
||||||
|
const peakThroughput = ref("15,234");
|
||||||
|
const avgThroughput = ref("9,876");
|
||||||
|
|
||||||
|
// Uptime metrics
|
||||||
|
const currentUptime = ref("99.97");
|
||||||
|
const uptimeDuration = ref("47 days, 12 hours");
|
||||||
|
const uptime24h = ref("100.00");
|
||||||
|
const uptime7d = ref("99.95");
|
||||||
|
const uptime30d = ref("99.92");
|
||||||
|
|
||||||
|
// Recent incidents
|
||||||
|
const recentIncidents = ref([
|
||||||
|
{
|
||||||
|
description: "Database connection timeout",
|
||||||
|
duration: "2m 15s",
|
||||||
|
timestamp: "2 days ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Worker node restart",
|
||||||
|
duration: "45s",
|
||||||
|
timestamp: "5 days ago"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Scaling configuration
|
||||||
|
const activeWorkers = ref(8);
|
||||||
|
const autoScalingEnabled = ref(true);
|
||||||
|
const queueLoad = ref(67);
|
||||||
|
|
||||||
|
const scalingConfig = ref({
|
||||||
|
autoScalingEnabled: true,
|
||||||
|
minWorkers: 2,
|
||||||
|
maxWorkers: 20,
|
||||||
|
scaleUpThreshold: 80,
|
||||||
|
scaleDownThreshold: 30
|
||||||
|
});
|
||||||
|
|
||||||
|
// Worker instances
|
||||||
|
const workers = ref([
|
||||||
|
{
|
||||||
|
id: "worker-001",
|
||||||
|
status: "healthy",
|
||||||
|
cpuUsage: 45,
|
||||||
|
memoryUsage: 62,
|
||||||
|
jobsProcessed: 15847,
|
||||||
|
lastHeartbeat: "2 seconds ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "worker-002",
|
||||||
|
status: "healthy",
|
||||||
|
cpuUsage: 52,
|
||||||
|
memoryUsage: 58,
|
||||||
|
jobsProcessed: 14923,
|
||||||
|
lastHeartbeat: "1 second ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "worker-003",
|
||||||
|
status: "warning",
|
||||||
|
cpuUsage: 78,
|
||||||
|
memoryUsage: 85,
|
||||||
|
jobsProcessed: 12456,
|
||||||
|
lastHeartbeat: "5 seconds ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "worker-004",
|
||||||
|
status: "healthy",
|
||||||
|
cpuUsage: 41,
|
||||||
|
memoryUsage: 55,
|
||||||
|
jobsProcessed: 16234,
|
||||||
|
lastHeartbeat: "1 second ago"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Performance alerts
|
||||||
|
const performanceAlerts = ref([
|
||||||
|
{
|
||||||
|
title: "High Queue Load",
|
||||||
|
description: "Queue load has exceeded 80% for the last 10 minutes",
|
||||||
|
severity: "warning",
|
||||||
|
icon: "ic:outline-warning",
|
||||||
|
timestamp: "5 minutes ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Worker High CPU Usage",
|
||||||
|
description: "Worker-003 CPU usage is at 78%",
|
||||||
|
severity: "warning",
|
||||||
|
icon: "ic:outline-memory",
|
||||||
|
timestamp: "2 minutes ago"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Throughput Target Met",
|
||||||
|
description: "Successfully processing 12,847 notifications per minute",
|
||||||
|
severity: "info",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
timestamp: "1 minute ago"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const refreshMetrics = () => {
|
||||||
|
console.log('Refreshing performance metrics...');
|
||||||
|
// Simulate data refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
const acknowledgeAlert = (index) => {
|
||||||
|
performanceAlerts.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveScalingConfig = () => {
|
||||||
|
console.log('Saving scaling configuration:', scalingConfig.value);
|
||||||
|
showScalingModal.value = false;
|
||||||
|
// Here you would typically send the config to your backend
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-refresh metrics every 30 seconds
|
||||||
|
onMounted(() => {
|
||||||
|
const interval = setInterval(refreshMetrics, 30000);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
803
pages/notification/queue-scheduler/persistence.vue
Normal file
803
pages/notification/queue-scheduler/persistence.vue
Normal file
@ -0,0 +1,803 @@
|
|||||||
|
<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>
|
665
pages/notification/queue-scheduler/priority.vue
Normal file
665
pages/notification/queue-scheduler/priority.vue
Normal file
@ -0,0 +1,665 @@
|
|||||||
|
<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-priority-high"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Priority Queue Management</h1>
|
||||||
|
</div>
|
||||||
|
<rs-button @click="showCreatePriorityModal = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-add"></Icon>
|
||||||
|
Create Priority Level
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Manage different priority levels for notifications to ensure critical messages are processed first.
|
||||||
|
Higher priority notifications will be processed before lower priority ones in the queue.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Priority Level 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 priorityStats"
|
||||||
|
: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-2xl leading-tight" :class="stat.valueColor">
|
||||||
|
{{ stat.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ stat.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Priority Levels Configuration -->
|
||||||
|
<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-settings"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Priority Levels</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshPriorityLevels">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="outline" size="sm" @click="showBulkEditModal = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-edit"></Icon>
|
||||||
|
Bulk Edit
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="(priority, index) in priorityLevels"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-4 border rounded-lg"
|
||||||
|
:class="{
|
||||||
|
'border-red-200 bg-red-50': priority.level === 'critical',
|
||||||
|
'border-orange-200 bg-orange-50': priority.level === 'high',
|
||||||
|
'border-yellow-200 bg-yellow-50': priority.level === 'medium',
|
||||||
|
'border-blue-200 bg-blue-50': priority.level === 'low',
|
||||||
|
'border-gray-200 bg-gray-50': priority.level === 'bulk'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-4 h-4 rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-red-500': priority.level === 'critical',
|
||||||
|
'bg-orange-500': priority.level === 'high',
|
||||||
|
'bg-yellow-500': priority.level === 'medium',
|
||||||
|
'bg-blue-500': priority.level === 'low',
|
||||||
|
'bg-gray-500': priority.level === 'bulk'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<span class="font-medium text-lg">{{ priority.name }}</span>
|
||||||
|
<span class="text-sm text-gray-500">({{ priority.level }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Weight</p>
|
||||||
|
<p class="font-bold">{{ priority.weight }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Queue Count</p>
|
||||||
|
<p class="font-bold">{{ priority.queueCount.toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Avg Processing</p>
|
||||||
|
<p class="font-bold">{{ priority.avgProcessingTime }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600">Status</p>
|
||||||
|
<span
|
||||||
|
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 text-green-800': priority.status === 'active',
|
||||||
|
'bg-red-100 text-red-800': priority.status === 'paused',
|
||||||
|
'bg-yellow-100 text-yellow-800': priority.status === 'throttled'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ priority.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="editPriority(priority)"
|
||||||
|
>
|
||||||
|
<Icon class="mr-1" name="ic:outline-edit"></Icon>
|
||||||
|
Edit
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:class="priority.status === 'active' ? 'text-red-600' : 'text-green-600'"
|
||||||
|
@click="togglePriorityStatus(priority)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="mr-1"
|
||||||
|
:name="priority.status === 'active' ? 'ic:outline-pause' : 'ic:outline-play-arrow'"
|
||||||
|
></Icon>
|
||||||
|
{{ priority.status === 'active' ? 'Pause' : 'Resume' }}
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Queue Processing Order -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-sort"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Processing Order Visualization</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
Notifications are processed in the following order based on priority weights:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Processing Flow -->
|
||||||
|
<div class="flex items-center justify-between bg-gray-50 p-4 rounded-lg">
|
||||||
|
<div
|
||||||
|
v-for="(level, index) in sortedPriorityLevels"
|
||||||
|
:key="index"
|
||||||
|
class="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-lg mb-2"
|
||||||
|
:class="{
|
||||||
|
'bg-red-500': level.level === 'critical',
|
||||||
|
'bg-orange-500': level.level === 'high',
|
||||||
|
'bg-yellow-500': level.level === 'medium',
|
||||||
|
'bg-blue-500': level.level === 'low',
|
||||||
|
'bg-gray-500': level.level === 'bulk'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ level.weight }}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">{{ level.name }}</span>
|
||||||
|
<span class="text-xs text-gray-500">{{ level.queueCount }} jobs</span>
|
||||||
|
|
||||||
|
<!-- Arrow -->
|
||||||
|
<Icon
|
||||||
|
v-if="index < sortedPriorityLevels.length - 1"
|
||||||
|
class="text-gray-400 mt-2"
|
||||||
|
name="ic:outline-arrow-forward"
|
||||||
|
></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Rules -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 class="font-medium text-blue-800 mb-2">Processing Rules</h4>
|
||||||
|
<ul class="text-sm text-blue-700 space-y-1">
|
||||||
|
<li>• Higher weight = Higher priority</li>
|
||||||
|
<li>• Critical jobs always processed first</li>
|
||||||
|
<li>• Same priority jobs use FIFO order</li>
|
||||||
|
<li>• Bulk jobs processed during low traffic</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h4 class="font-medium text-green-800 mb-2">Performance Impact</h4>
|
||||||
|
<ul class="text-sm text-green-700 space-y-1">
|
||||||
|
<li>• Critical: < 1 second processing</li>
|
||||||
|
<li>• High: < 5 seconds processing</li>
|
||||||
|
<li>• Medium: < 30 seconds processing</li>
|
||||||
|
<li>• Low/Bulk: Best effort processing</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Recent Priority Queue Activity -->
|
||||||
|
<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-history"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Priority Queue Activity</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button variant="outline" size="sm" @click="navigateTo('/notification/queue-scheduler/monitor')">
|
||||||
|
View Full Monitor
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Job ID
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Priority
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Queue Time
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Processing Time
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr v-for="(job, index) in recentJobs" :key="index">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{{ job.id }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-red-100 text-red-800': job.priority === 'critical',
|
||||||
|
'bg-orange-100 text-orange-800': job.priority === 'high',
|
||||||
|
'bg-yellow-100 text-yellow-800': job.priority === 'medium',
|
||||||
|
'bg-blue-100 text-blue-800': job.priority === 'low',
|
||||||
|
'bg-gray-100 text-gray-800': job.priority === 'bulk'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ job.priority }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{{ job.type }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 text-green-800': job.status === 'completed',
|
||||||
|
'bg-yellow-100 text-yellow-800': job.status === 'processing',
|
||||||
|
'bg-red-100 text-red-800': job.status === 'failed',
|
||||||
|
'bg-blue-100 text-blue-800': job.status === 'queued'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ job.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{{ job.queueTime }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{{ job.processingTime }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Create Priority Level Modal -->
|
||||||
|
<rs-modal v-model="showCreatePriorityModal" title="Create Priority Level">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Priority Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="newPriority.name"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="e.g., Emergency Alerts"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Priority Level</label>
|
||||||
|
<select v-model="newPriority.level" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="critical">Critical</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="bulk">Bulk</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Weight (1-100)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="newPriority.weight"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Higher weight = Higher priority</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||||
|
<textarea
|
||||||
|
v-model="newPriority.description"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe when this priority level should be used..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Max Processing Time (seconds)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="newPriority.maxProcessingTime"
|
||||||
|
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 processing jobs of this priority</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<rs-button variant="outline" @click="showCreatePriorityModal = false">
|
||||||
|
Cancel
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="createPriorityLevel">
|
||||||
|
Create Priority Level
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
|
||||||
|
<!-- Edit Priority Modal -->
|
||||||
|
<rs-modal v-model="showEditPriorityModal" title="Edit Priority Level">
|
||||||
|
<div class="space-y-6" v-if="editingPriority">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Priority Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="editingPriority.name"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Weight (1-100)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="editingPriority.weight"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Max Processing Time (seconds)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="editingPriority.maxProcessingTime"
|
||||||
|
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">Status</label>
|
||||||
|
<select v-model="editingPriority.status" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="paused">Paused</option>
|
||||||
|
<option value="throttled">Throttled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<rs-button variant="outline" @click="showEditPriorityModal = false">
|
||||||
|
Cancel
|
||||||
|
</rs-button>
|
||||||
|
<rs-button @click="savePriorityChanges">
|
||||||
|
Save Changes
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Priority Queue Management",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Priority Management",
|
||||||
|
path: "/notification/queue-scheduler/priority",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const showCreatePriorityModal = ref(false);
|
||||||
|
const showEditPriorityModal = ref(false);
|
||||||
|
const showBulkEditModal = ref(false);
|
||||||
|
const editingPriority = ref(null);
|
||||||
|
|
||||||
|
// Priority statistics
|
||||||
|
const priorityStats = ref([
|
||||||
|
{
|
||||||
|
title: "Critical Jobs",
|
||||||
|
value: "47",
|
||||||
|
icon: "ic:outline-priority-high",
|
||||||
|
bgColor: "bg-red-100",
|
||||||
|
iconColor: "text-red-600",
|
||||||
|
valueColor: "text-red-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "High Priority",
|
||||||
|
value: "234",
|
||||||
|
icon: "ic:outline-trending-up",
|
||||||
|
bgColor: "bg-orange-100",
|
||||||
|
iconColor: "text-orange-600",
|
||||||
|
valueColor: "text-orange-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medium Priority",
|
||||||
|
value: "1,847",
|
||||||
|
icon: "ic:outline-remove",
|
||||||
|
bgColor: "bg-yellow-100",
|
||||||
|
iconColor: "text-yellow-600",
|
||||||
|
valueColor: "text-yellow-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Low/Bulk Priority",
|
||||||
|
value: "5,923",
|
||||||
|
icon: "ic:outline-trending-down",
|
||||||
|
bgColor: "bg-blue-100",
|
||||||
|
iconColor: "text-blue-600",
|
||||||
|
valueColor: "text-blue-600"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Priority levels configuration
|
||||||
|
const priorityLevels = ref([
|
||||||
|
{
|
||||||
|
name: "Emergency Alerts",
|
||||||
|
level: "critical",
|
||||||
|
weight: 100,
|
||||||
|
queueCount: 47,
|
||||||
|
avgProcessingTime: "0.8s",
|
||||||
|
status: "active",
|
||||||
|
maxProcessingTime: 5,
|
||||||
|
description: "System emergencies and critical security alerts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Real-time Notifications",
|
||||||
|
level: "high",
|
||||||
|
weight: 80,
|
||||||
|
queueCount: 234,
|
||||||
|
avgProcessingTime: "2.1s",
|
||||||
|
status: "active",
|
||||||
|
maxProcessingTime: 10,
|
||||||
|
description: "Time-sensitive notifications like OTP, payment confirmations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard Notifications",
|
||||||
|
level: "medium",
|
||||||
|
weight: 50,
|
||||||
|
queueCount: 1847,
|
||||||
|
avgProcessingTime: "5.3s",
|
||||||
|
status: "active",
|
||||||
|
maxProcessingTime: 30,
|
||||||
|
description: "Regular app notifications and updates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Marketing Messages",
|
||||||
|
level: "low",
|
||||||
|
weight: 30,
|
||||||
|
queueCount: 3421,
|
||||||
|
avgProcessingTime: "12.7s",
|
||||||
|
status: "active",
|
||||||
|
maxProcessingTime: 60,
|
||||||
|
description: "Promotional content and marketing campaigns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bulk Operations",
|
||||||
|
level: "bulk",
|
||||||
|
weight: 10,
|
||||||
|
queueCount: 2502,
|
||||||
|
avgProcessingTime: "45.2s",
|
||||||
|
status: "throttled",
|
||||||
|
maxProcessingTime: 300,
|
||||||
|
description: "Large batch operations and system maintenance"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// New priority form
|
||||||
|
const newPriority = ref({
|
||||||
|
name: "",
|
||||||
|
level: "medium",
|
||||||
|
weight: 50,
|
||||||
|
description: "",
|
||||||
|
maxProcessingTime: 30
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed sorted priority levels
|
||||||
|
const sortedPriorityLevels = computed(() => {
|
||||||
|
return [...priorityLevels.value].sort((a, b) => b.weight - a.weight);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recent jobs data
|
||||||
|
const recentJobs = ref([
|
||||||
|
{
|
||||||
|
id: "job-001",
|
||||||
|
priority: "critical",
|
||||||
|
type: "Security Alert",
|
||||||
|
status: "completed",
|
||||||
|
queueTime: "0.1s",
|
||||||
|
processingTime: "0.8s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-002",
|
||||||
|
priority: "high",
|
||||||
|
type: "OTP SMS",
|
||||||
|
status: "completed",
|
||||||
|
queueTime: "0.3s",
|
||||||
|
processingTime: "1.2s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-003",
|
||||||
|
priority: "medium",
|
||||||
|
type: "App Notification",
|
||||||
|
status: "processing",
|
||||||
|
queueTime: "2.1s",
|
||||||
|
processingTime: "3.4s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-004",
|
||||||
|
priority: "low",
|
||||||
|
type: "Newsletter",
|
||||||
|
status: "queued",
|
||||||
|
queueTime: "15.2s",
|
||||||
|
processingTime: "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-005",
|
||||||
|
priority: "bulk",
|
||||||
|
type: "Data Export",
|
||||||
|
status: "queued",
|
||||||
|
queueTime: "45.7s",
|
||||||
|
processingTime: "-"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const refreshPriorityLevels = () => {
|
||||||
|
console.log('Refreshing priority levels...');
|
||||||
|
// Simulate data refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPriorityLevel = () => {
|
||||||
|
console.log('Creating priority level:', newPriority.value);
|
||||||
|
|
||||||
|
// Add to priority levels
|
||||||
|
priorityLevels.value.push({
|
||||||
|
...newPriority.value,
|
||||||
|
queueCount: 0,
|
||||||
|
avgProcessingTime: "0s",
|
||||||
|
status: "active"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
newPriority.value = {
|
||||||
|
name: "",
|
||||||
|
level: "medium",
|
||||||
|
weight: 50,
|
||||||
|
description: "",
|
||||||
|
maxProcessingTime: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
showCreatePriorityModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editPriority = (priority) => {
|
||||||
|
editingPriority.value = { ...priority };
|
||||||
|
showEditPriorityModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePriorityChanges = () => {
|
||||||
|
const index = priorityLevels.value.findIndex(p => p.name === editingPriority.value.name);
|
||||||
|
if (index !== -1) {
|
||||||
|
priorityLevels.value[index] = { ...editingPriority.value };
|
||||||
|
}
|
||||||
|
showEditPriorityModal.value = false;
|
||||||
|
editingPriority.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePriorityStatus = (priority) => {
|
||||||
|
const newStatus = priority.status === 'active' ? 'paused' : 'active';
|
||||||
|
priority.status = newStatus;
|
||||||
|
console.log(`Priority ${priority.name} status changed to ${newStatus}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
Loading…
x
Reference in New Issue
Block a user