661 lines
20 KiB
Vue

<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">Delivery Monitor</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Real-time monitoring of notification deliveries across all channels. Track individual messages,
monitor batch progress, and analyze delivery performance with detailed metrics.
</p>
</template>
</rs-card>
<!-- Real-time Metrics -->
<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 realTimeMetrics"
: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="metric.bgColor"
>
<Icon class="text-3xl" :name="metric.icon" :class="metric.iconColor"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl leading-tight" :class="metric.textColor">
{{ metric.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ metric.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Message Search & Filter -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-primary">Message Tracking</h2>
<div class="flex gap-2">
<rs-button size="sm" variant="primary-outline" @click="refreshMessages">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
<rs-button size="sm" variant="primary-outline" @click="showFilters = !showFilters">
<Icon class="mr-1" name="ic:outline-filter-list"></Icon>
Filters
</rs-button>
</div>
</div>
</template>
<template #body>
<!-- Search and Filters -->
<div class="mb-6 space-y-4">
<div class="flex gap-4">
<div class="flex-1">
<FormKit
type="text"
v-model="searchQuery"
placeholder="Search by message ID, recipient, or content..."
prefix-icon="search"
/>
</div>
<div class="w-48">
<FormKit
type="select"
v-model="selectedChannel"
:options="channelFilterOptions"
placeholder="All Channels"
/>
</div>
</div>
<!-- Advanced Filters -->
<div v-if="showFilters" class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg">
<FormKit
type="select"
v-model="filters.status"
:options="statusFilterOptions"
label="Status"
/>
<FormKit
type="select"
v-model="filters.priority"
:options="priorityFilterOptions"
label="Priority"
/>
<FormKit
type="date"
v-model="filters.dateFrom"
label="From Date"
/>
<FormKit
type="date"
v-model="filters.dateTo"
label="To Date"
/>
</div>
</div>
<!-- Messages Table -->
<rs-table
:field="messageTableFields"
:data="filteredMessages"
:advanced="true"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{
sortable: true,
filterable: true,
responsive: true,
}"
:pageSize="20"
>
<template #messageId="{ row }">
<button
@click="viewMessageDetails(row)"
class="text-primary hover:underline font-mono text-sm"
>
{{ row.messageId }}
</button>
</template>
<template #status="{ row }">
<rs-badge :variant="getStatusVariant(row.status)" size="sm">
{{ row.status }}
</rs-badge>
</template>
<template #channel="{ row }">
<div class="flex items-center">
<Icon class="mr-2" :name="getChannelIcon(row.channel)"></Icon>
{{ row.channel }}
</div>
</template>
<template #priority="{ row }">
<rs-badge
:variant="getPriorityVariant(row.priority)"
size="sm"
>
{{ row.priority }}
</rs-badge>
</template>
<template #progress="{ row }">
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: row.progress + '%' }"
></div>
</div>
<div class="text-xs text-gray-500 mt-1">{{ row.progress }}%</div>
</template>
<template #actions="{ row }">
<div class="flex gap-2">
<rs-button
size="sm"
variant="primary-outline"
@click="viewMessageDetails(row)"
>
<Icon name="ic:outline-visibility"></Icon>
</rs-button>
<rs-button
size="sm"
variant="secondary-outline"
@click="retryMessage(row)"
v-if="row.status === 'failed'"
>
<Icon name="ic:outline-refresh"></Icon>
</rs-button>
</div>
</template>
</rs-table>
</template>
</rs-card>
<!-- Live Activity Feed -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Real-time 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-live-tv"></Icon>
<h2 class="text-lg font-semibold text-primary">Live Activity</h2>
</div>
<rs-badge variant="success" size="sm">Live</rs-badge>
</div>
</template>
<template #body>
<div class="space-y-3 max-h-96 overflow-y-auto">
<div
v-for="activity in liveActivities"
:key="activity.id"
class="flex items-start gap-3 p-3 border border-gray-200 rounded-lg"
>
<Icon
class="mt-1 flex-shrink-0"
:name="activity.icon"
:class="activity.iconColor"
></Icon>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm">{{ activity.message }}</div>
<div class="text-xs text-gray-500">{{ activity.timestamp }}</div>
<div v-if="activity.details" class="text-xs text-gray-600 mt-1">
{{ activity.details }}
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Channel Performance -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
<h2 class="text-lg font-semibold text-primary">Channel Performance</h2>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="channel in channelPerformance"
:key="channel.name"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<Icon class="mr-2" :name="channel.icon"></Icon>
<span class="font-semibold">{{ channel.name }}</span>
</div>
<rs-badge :variant="channel.statusVariant" size="sm">
{{ channel.status }}
</rs-badge>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-gray-600">Throughput</div>
<div class="font-semibold">{{ channel.throughput }}/min</div>
</div>
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ channel.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Avg Latency</div>
<div class="font-semibold">{{ channel.avgLatency }}ms</div>
</div>
<div>
<div class="text-gray-600">Queue Size</div>
<div class="font-semibold">{{ channel.queueSize }}</div>
</div>
</div>
<!-- Performance Chart -->
<div class="mt-3">
<div class="text-xs text-gray-500 mb-1">Performance Trend</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="h-2 rounded-full transition-all duration-300"
:class="channel.performanceClass"
:style="{ width: channel.performanceScore + '%' }"
></div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Message Details Modal -->
<rs-modal v-model="showMessageModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Message Details</h3>
</template>
<template #body>
<div v-if="selectedMessage" class="space-y-6">
<!-- Message Info -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Message ID</label>
<div class="font-mono text-sm">{{ selectedMessage.messageId }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
<rs-badge :variant="getStatusVariant(selectedMessage.status)">
{{ selectedMessage.status }}
</rs-badge>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
<div class="flex items-center">
<Icon class="mr-2" :name="getChannelIcon(selectedMessage.channel)"></Icon>
{{ selectedMessage.channel }}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Priority</label>
<rs-badge :variant="getPriorityVariant(selectedMessage.priority)">
{{ selectedMessage.priority }}
</rs-badge>
</div>
</div>
<!-- Delivery Timeline -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-3">Delivery Timeline</label>
<div class="space-y-3">
<div
v-for="event in selectedMessage.timeline"
:key="event.id"
class="flex items-center gap-3"
>
<div class="w-3 h-3 rounded-full" :class="event.statusColor"></div>
<div class="flex-1">
<div class="font-medium text-sm">{{ event.status }}</div>
<div class="text-xs text-gray-500">{{ event.timestamp }}</div>
<div v-if="event.details" class="text-xs text-gray-600">{{ event.details }}</div>
</div>
</div>
</div>
</div>
<!-- Message Content -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Content</label>
<div class="bg-gray-50 rounded-lg p-3 text-sm">
<div><strong>To:</strong> {{ selectedMessage.recipient }}</div>
<div><strong>Subject:</strong> {{ selectedMessage.subject }}</div>
<div class="mt-2"><strong>Body:</strong></div>
<div class="mt-1">{{ selectedMessage.content }}</div>
</div>
</div>
<!-- Provider Response -->
<div v-if="selectedMessage.providerResponse">
<label class="block text-sm font-medium text-gray-500 mb-1">Provider Response</label>
<div class="bg-gray-50 rounded-lg p-3">
<pre class="text-xs">{{ JSON.stringify(selectedMessage.providerResponse, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showMessageModal = false">Close</rs-button>
<rs-button
variant="primary"
@click="retryMessage(selectedMessage)"
v-if="selectedMessage?.status === 'failed'"
>
Retry Message
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Delivery Monitor",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/dashboard",
},
{
name: "Notification",
path: "/notification",
},
{
name: "Delivery Engine",
path: "/notification/delivery",
},
{
name: "Monitor",
path: "/notification/delivery/monitor",
type: "current",
},
],
});
import { ref, computed, onMounted, onUnmounted } from "vue";
// State
const showFilters = ref(false);
const showMessageModal = ref(false);
const selectedMessage = ref(null);
const searchQuery = ref("");
const selectedChannel = ref("");
// Filters
const filters = ref({
status: "",
priority: "",
dateFrom: "",
dateTo: "",
});
// Real-time metrics
const realTimeMetrics = ref([
{
title: "Messages/Min",
value: "1,247",
icon: "ic:outline-speed",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600",
},
{
title: "Success Rate",
value: "98.7%",
icon: "ic:outline-check-circle",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600",
},
{
title: "Failed/Retrying",
value: "23",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600",
},
{
title: "Avg Latency",
value: "1.2s",
icon: "ic:outline-timer",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600",
},
]);
// Filter options
const channelFilterOptions = [
{ label: "All Channels", value: "" },
{ label: "Email", value: "email" },
{ label: "SMS", value: "sms" },
{ label: "Push", value: "push" },
{ label: "In-App", value: "inapp" },
];
const statusFilterOptions = [
{ label: "All Status", value: "" },
{ label: "Queued", value: "queued" },
{ label: "Sent", value: "sent" },
{ label: "Delivered", value: "delivered" },
{ label: "Opened", value: "opened" },
{ label: "Failed", value: "failed" },
{ label: "Bounced", value: "bounced" },
];
const priorityFilterOptions = [
{ label: "All Priorities", value: "" },
{ label: "Critical", value: "critical" },
{ label: "High", value: "high" },
{ label: "Medium", value: "medium" },
{ label: "Low", value: "low" },
];
// Table configuration
const messageTableFields = ref([
{ key: "messageId", label: "Message ID", sortable: true },
{ key: "recipient", label: "Recipient", sortable: true },
{ key: "channel", label: "Channel", sortable: true },
{ key: "status", label: "Status", sortable: true },
{ key: "priority", label: "Priority", sortable: true },
{ key: "createdAt", label: "Created", sortable: true },
{ key: "progress", label: "Progress", sortable: false },
{ key: "actions", label: "Actions", sortable: false },
]);
// Sample messages data
const messages = ref([
{
messageId: "msg_001",
recipient: "user@example.com",
channel: "Email",
status: "delivered",
priority: "high",
createdAt: "2024-01-15 10:30:00",
progress: 100,
subject: "Welcome to our platform",
content: "Thank you for joining us...",
timeline: [
{ id: 1, status: "Queued", timestamp: "2024-01-15 10:30:00", statusColor: "bg-blue-500", details: "Message queued for processing" },
{ id: 2, status: "Sent", timestamp: "2024-01-15 10:30:15", statusColor: "bg-yellow-500", details: "Sent via SendGrid" },
{ id: 3, status: "Delivered", timestamp: "2024-01-15 10:30:18", statusColor: "bg-green-500", details: "Successfully delivered" },
],
providerResponse: { messageId: "sg_abc123", status: "delivered" },
},
// Add more sample data...
]);
// Live activities
const liveActivities = ref([
{
id: 1,
message: "Email batch completed",
timestamp: "Just now",
icon: "ic:outline-email",
iconColor: "text-green-500",
details: "1,250 emails sent successfully",
},
{
id: 2,
message: "SMS delivery in progress",
timestamp: "2 seconds ago",
icon: "ic:outline-sms",
iconColor: "text-blue-500",
details: "89/120 messages delivered",
},
// Add more activities...
]);
// Channel performance
const channelPerformance = ref([
{
name: "Email",
icon: "ic:outline-email",
status: "Healthy",
statusVariant: "success",
throughput: "1,200",
successRate: 99.2,
avgLatency: 800,
queueSize: 45,
performanceScore: 95,
performanceClass: "bg-green-500",
},
{
name: "SMS",
icon: "ic:outline-sms",
status: "Warning",
statusVariant: "warning",
throughput: "450",
successRate: 97.8,
avgLatency: 2100,
queueSize: 123,
performanceScore: 78,
performanceClass: "bg-yellow-500",
},
// Add more channels...
]);
// Computed
const filteredMessages = computed(() => {
let filtered = messages.value;
if (searchQuery.value) {
filtered = filtered.filter(msg =>
msg.messageId.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
msg.recipient.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
if (selectedChannel.value) {
filtered = filtered.filter(msg => msg.channel.toLowerCase() === selectedChannel.value);
}
if (filters.value.status) {
filtered = filtered.filter(msg => msg.status === filters.value.status);
}
if (filters.value.priority) {
filtered = filtered.filter(msg => msg.priority === filters.value.priority);
}
return filtered;
});
// Methods
function getStatusVariant(status) {
const variants = {
queued: "info",
sent: "warning",
delivered: "success",
opened: "success",
failed: "danger",
bounced: "danger",
};
return variants[status] || "secondary";
}
function getChannelIcon(channel) {
const icons = {
Email: "ic:outline-email",
SMS: "ic:outline-sms",
Push: "ic:outline-notifications",
"In-App": "ic:outline-app-registration",
};
return icons[channel] || "ic:outline-help";
}
function getPriorityVariant(priority) {
const variants = {
critical: "danger",
high: "warning",
medium: "info",
low: "secondary",
};
return variants[priority] || "secondary";
}
function viewMessageDetails(message) {
selectedMessage.value = message;
showMessageModal.value = true;
}
function retryMessage(message) {
console.log("Retrying message:", message.messageId);
// Implementation for retrying failed messages
}
function refreshMessages() {
console.log("Refreshing messages...");
// Implementation for refreshing message list
}
// Auto-refresh every 10 seconds
let refreshInterval;
onMounted(() => {
refreshInterval = setInterval(() => {
refreshMessages();
}, 10000);
});
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
<style lang="scss" scoped></style>