Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
44
server/api/notifications/logs/[id].get.js
Normal file
44
server/api/notifications/logs/[id].get.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = event.context.user;
|
||||
if (!user || !user.userID) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: { success: false, message: 'Unauthorized' }
|
||||
}
|
||||
}
|
||||
|
||||
const id = event.context.params.id
|
||||
if (!id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { success: false, message: 'Log ID is required' }
|
||||
}
|
||||
}
|
||||
|
||||
const log = await prisma.notification_logs.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!log) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
body: { success: false, message: 'Log entry not found' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { success: true, data: log }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching log entry:', error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: { success: false, message: 'Failed to fetch log entry', error: error.message }
|
||||
}
|
||||
}
|
||||
})
|
||||
492
server/api/notifications/logs/analytics.get.js
Normal file
492
server/api/notifications/logs/analytics.get.js
Normal file
@@ -0,0 +1,492 @@
|
||||
import prisma from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = event.context.user;
|
||||
if (!user || !user.userID) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: { success: false, message: 'Unauthorized' }
|
||||
}
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const period = query.period || '7d' // Default to last 7 days
|
||||
const channel = query.channel || 'all'
|
||||
|
||||
// Calculate date range based on selected period
|
||||
const endDate = new Date()
|
||||
let startDate = new Date()
|
||||
|
||||
switch(period) {
|
||||
case '1d':
|
||||
startDate.setDate(startDate.getDate() - 1)
|
||||
break
|
||||
case '7d':
|
||||
startDate.setDate(startDate.getDate() - 7)
|
||||
break
|
||||
case '30d':
|
||||
startDate.setDate(startDate.getDate() - 30)
|
||||
break
|
||||
case '90d':
|
||||
startDate.setDate(startDate.getDate() - 90)
|
||||
break
|
||||
case '12m':
|
||||
startDate.setMonth(startDate.getMonth() - 12)
|
||||
break
|
||||
default:
|
||||
startDate.setDate(startDate.getDate() - 7)
|
||||
}
|
||||
|
||||
// Channel filter
|
||||
const channelFilter = channel !== 'all' ? { channel_type: channel } : {}
|
||||
|
||||
// Get key metrics
|
||||
const totalSent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const previousPeriodEnd = new Date(startDate)
|
||||
const previousPeriodStart = new Date(startDate)
|
||||
|
||||
// Calculate the same duration for previous period
|
||||
switch(period) {
|
||||
case '1d':
|
||||
previousPeriodStart.setDate(previousPeriodStart.getDate() - 1)
|
||||
break
|
||||
case '7d':
|
||||
previousPeriodStart.setDate(previousPeriodStart.getDate() - 7)
|
||||
break
|
||||
case '30d':
|
||||
previousPeriodStart.setDate(previousPeriodStart.getDate() - 30)
|
||||
break
|
||||
case '90d':
|
||||
previousPeriodStart.setDate(previousPeriodStart.getDate() - 90)
|
||||
break
|
||||
case '12m':
|
||||
previousPeriodStart.setMonth(previousPeriodStart.getMonth() - 12)
|
||||
break
|
||||
}
|
||||
|
||||
const previousTotalSent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: previousPeriodStart,
|
||||
lt: startDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const sentChangePercent = previousTotalSent > 0
|
||||
? ((totalSent - previousTotalSent) / previousTotalSent * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
// Get success rate
|
||||
const totalAttempted = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
action: {
|
||||
in: ['Notification Sent', 'Delivery Attempted']
|
||||
},
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const successfulDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const successRate = totalAttempted > 0
|
||||
? ((successfulDeliveries / totalAttempted) * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
const previousSuccessRate = await calculateSuccessRate(
|
||||
prisma,
|
||||
previousPeriodStart,
|
||||
startDate,
|
||||
channelFilter
|
||||
)
|
||||
|
||||
const successRateChangePercent = previousSuccessRate > 0
|
||||
? ((parseFloat(successRate) - previousSuccessRate) / previousSuccessRate * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
// Get open rate (if tracking available)
|
||||
const totalOpened = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Opened',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const openRate = totalSent > 0
|
||||
? ((totalOpened / totalSent) * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
const previousOpenRate = await calculateOpenRate(
|
||||
prisma,
|
||||
previousPeriodStart,
|
||||
startDate,
|
||||
channelFilter
|
||||
)
|
||||
|
||||
const openRateChangePercent = previousOpenRate > 0
|
||||
? ((parseFloat(openRate) - previousOpenRate) / previousOpenRate * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
// Get click rate (if tracking available)
|
||||
const totalClicked = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Clicked',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const clickRate = totalSent > 0
|
||||
? ((totalClicked / totalSent) * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
const previousClickRate = await calculateClickRate(
|
||||
prisma,
|
||||
previousPeriodStart,
|
||||
startDate,
|
||||
channelFilter
|
||||
)
|
||||
|
||||
const clickRateChangePercent = previousClickRate > 0
|
||||
? ((parseFloat(clickRate) - previousClickRate) / previousClickRate * 100).toFixed(1)
|
||||
: '0.0'
|
||||
|
||||
// Get channel performance data
|
||||
const channelPerformance = await getChannelPerformance(prisma, startDate, endDate)
|
||||
|
||||
// Get recent notable events
|
||||
const recentEvents = await getRecentEvents(prisma)
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
data: {
|
||||
keyMetrics: [
|
||||
{
|
||||
title: "Total Sent",
|
||||
value: totalSent.toLocaleString(),
|
||||
icon: "ic:outline-send",
|
||||
trend: parseFloat(sentChangePercent) >= 0 ? "up" : "down",
|
||||
change: `${parseFloat(sentChangePercent) >= 0 ? '+' : ''}${sentChangePercent}%`
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
value: `${successRate}%`,
|
||||
icon: "ic:outline-check-circle",
|
||||
trend: parseFloat(successRateChangePercent) >= 0 ? "up" : "down",
|
||||
change: `${parseFloat(successRateChangePercent) >= 0 ? '+' : ''}${successRateChangePercent}%`
|
||||
},
|
||||
{
|
||||
title: "Open Rate",
|
||||
value: `${openRate}%`,
|
||||
icon: "ic:outline-open-in-new",
|
||||
trend: parseFloat(openRateChangePercent) >= 0 ? "up" : "down",
|
||||
change: `${parseFloat(openRateChangePercent) >= 0 ? '+' : ''}${openRateChangePercent}%`
|
||||
},
|
||||
{
|
||||
title: "Click Rate",
|
||||
value: `${clickRate}%`,
|
||||
icon: "ic:outline-touch-app",
|
||||
trend: parseFloat(clickRateChangePercent) >= 0 ? "up" : "down",
|
||||
change: `${parseFloat(clickRateChangePercent) >= 0 ? '+' : ''}${clickRateChangePercent}%`
|
||||
}
|
||||
],
|
||||
channelPerformance,
|
||||
recentEvents
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching analytics data:', error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: { success: false, message: 'Failed to fetch analytics data', error: error.message }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
async function calculateSuccessRate(prisma, startDate, endDate, channelFilter) {
|
||||
const totalAttempted = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
action: {
|
||||
in: ['Notification Sent', 'Delivery Attempted']
|
||||
},
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const successfulDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
return totalAttempted > 0
|
||||
? ((successfulDeliveries / totalAttempted) * 100)
|
||||
: 0
|
||||
}
|
||||
|
||||
async function calculateOpenRate(prisma, startDate, endDate, channelFilter) {
|
||||
const totalSent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const totalOpened = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
status: 'Opened',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
return totalSent > 0
|
||||
? ((totalOpened / totalSent) * 100)
|
||||
: 0
|
||||
}
|
||||
|
||||
async function calculateClickRate(prisma, startDate, endDate, channelFilter) {
|
||||
const totalSent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
const totalClicked = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lt: endDate
|
||||
},
|
||||
status: 'Clicked',
|
||||
...channelFilter
|
||||
}
|
||||
})
|
||||
|
||||
return totalSent > 0
|
||||
? ((totalClicked / totalSent) * 100)
|
||||
: 0
|
||||
}
|
||||
|
||||
async function getChannelPerformance(prisma, startDate, endDate) {
|
||||
// Define the channels we want to analyze
|
||||
const channels = ['Email', 'SMS', 'Push Notification', 'Webhook']
|
||||
const channelIcons = {
|
||||
'Email': 'ic:outline-email',
|
||||
'SMS': 'ic:outline-sms',
|
||||
'Push Notification': 'ic:outline-notifications',
|
||||
'Webhook': 'ic:outline-webhook'
|
||||
}
|
||||
|
||||
const result = []
|
||||
|
||||
for (const channel of channels) {
|
||||
// Get total sent for this channel
|
||||
const sent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Sent',
|
||||
channel_type: channel
|
||||
}
|
||||
})
|
||||
|
||||
// Get total failed for this channel
|
||||
const failed = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Failed',
|
||||
channel_type: channel
|
||||
}
|
||||
})
|
||||
|
||||
// Get total bounced for this channel
|
||||
const bounced = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Bounced',
|
||||
channel_type: channel
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate total attempted (sent + failed + bounced)
|
||||
const total = sent + failed + bounced
|
||||
|
||||
// Calculate success rate
|
||||
const successRate = total > 0 ? ((sent / total) * 100).toFixed(1) : '0.0'
|
||||
|
||||
// Calculate bounce rate
|
||||
const bounceRate = total > 0 ? ((bounced / total) * 100).toFixed(1) : '0.0'
|
||||
|
||||
// Calculate failure rate
|
||||
const failureRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0'
|
||||
|
||||
result.push({
|
||||
name: channel,
|
||||
icon: channelIcons[channel] || 'ic:outline-message',
|
||||
sent: sent.toString(),
|
||||
failed: failed.toString(),
|
||||
bounced: bounced.toString(),
|
||||
total: total.toString(),
|
||||
successRate,
|
||||
bounceRate,
|
||||
failureRate
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Function to get recent notable events
|
||||
async function getRecentEvents(prisma) {
|
||||
// Get last 24 hours
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now)
|
||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
||||
|
||||
// Find recent logs with interesting events
|
||||
const recentLogs = await prisma.notification_logs.findMany({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: oneDayAgo,
|
||||
lte: now
|
||||
},
|
||||
OR: [
|
||||
{ status: 'Failed' },
|
||||
{ status: 'Bounced' },
|
||||
{
|
||||
AND: [
|
||||
{ status: 'Sent' },
|
||||
{
|
||||
details: {
|
||||
contains: 'batch'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ status: 'Opened' }
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
},
|
||||
take: 5
|
||||
})
|
||||
|
||||
// Transform logs into notable events
|
||||
return recentLogs.map(log => {
|
||||
let type = 'info'
|
||||
let title = log.action
|
||||
let description = log.details || ''
|
||||
let value = ''
|
||||
let time = formatTimeAgo(log.created_at)
|
||||
|
||||
if (log.status === 'Failed') {
|
||||
type = 'error'
|
||||
title = 'Delivery Failure'
|
||||
description = log.error_message || log.details || ''
|
||||
value = log.channel_type || ''
|
||||
} else if (log.status === 'Bounced') {
|
||||
type = 'warning'
|
||||
title = 'Delivery Bounced'
|
||||
value = log.channel_type || ''
|
||||
} else if (log.status === 'Opened') {
|
||||
type = 'success'
|
||||
title = 'Notification Opened'
|
||||
value = 'User Engagement'
|
||||
} else if (log.status === 'Sent' && log.details?.includes('batch')) {
|
||||
type = 'info'
|
||||
title = 'Batch Delivery'
|
||||
// Try to extract batch size from details
|
||||
const match = log.details?.match(/(\d+)\s*notifications?/i)
|
||||
value = match ? `${match[1]} sent` : ''
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
time,
|
||||
type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Format time ago
|
||||
function 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`
|
||||
}
|
||||
234
server/api/notifications/logs/index.get.js
Normal file
234
server/api/notifications/logs/index.get.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import prisma from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = event.context.user;
|
||||
if (!user || !user.userID) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: { success: false, message: 'Unauthorized' }
|
||||
}
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
let logs = [];
|
||||
let totalLogs = 0;
|
||||
let failedDeliveries = 0;
|
||||
let successfulDeliveries = 0;
|
||||
let total = 0;
|
||||
|
||||
try {
|
||||
// Define filters
|
||||
const filters = {}
|
||||
|
||||
// Date range filter
|
||||
if (query.startDate) {
|
||||
filters.created_at = {
|
||||
...filters.created_at,
|
||||
gte: new Date(query.startDate)
|
||||
}
|
||||
}
|
||||
|
||||
if (query.endDate) {
|
||||
filters.created_at = {
|
||||
...filters.created_at,
|
||||
lte: new Date(query.endDate)
|
||||
}
|
||||
}
|
||||
|
||||
// Action filter
|
||||
if (query.action) {
|
||||
filters.action = query.action
|
||||
}
|
||||
|
||||
// Channel filter
|
||||
if (query.channel) {
|
||||
filters.channel_type = query.channel
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (query.status) {
|
||||
filters.status = query.status
|
||||
}
|
||||
|
||||
// Actor/user filter
|
||||
if (query.actor) {
|
||||
filters.actor = {
|
||||
contains: query.actor
|
||||
}
|
||||
}
|
||||
|
||||
// Notification ID filter
|
||||
if (query.notificationId) {
|
||||
filters.notification_id = query.notificationId
|
||||
}
|
||||
|
||||
// Keyword search in details or error_message
|
||||
if (query.keyword) {
|
||||
filters.OR = [
|
||||
{ details: { contains: query.keyword } },
|
||||
{ error_message: { contains: query.keyword } }
|
||||
]
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const page = parseInt(query.page) || 1
|
||||
const limit = parseInt(query.limit) || 10
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
try {
|
||||
console.log("Attempting to query notification_logs table...");
|
||||
|
||||
// First check if the table exists
|
||||
let tableExists = true;
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1 FROM notification_logs LIMIT 1`;
|
||||
console.log("notification_logs table exists!");
|
||||
} catch (tableCheckError) {
|
||||
console.error("Table check error:", tableCheckError.message);
|
||||
console.error("Table likely doesn't exist - you need to run the migration!");
|
||||
tableExists = false;
|
||||
throw new Error("notification_logs table does not exist");
|
||||
}
|
||||
|
||||
if (tableExists) {
|
||||
// Get total count for pagination
|
||||
total = await prisma.notification_logs.count({
|
||||
where: filters
|
||||
})
|
||||
|
||||
// Get logs with pagination and sorting
|
||||
logs = await prisma.notification_logs.findMany({
|
||||
where: filters,
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
},
|
||||
skip,
|
||||
take: limit
|
||||
})
|
||||
|
||||
// Get summary stats
|
||||
totalLogs = await prisma.notification_logs.count()
|
||||
failedDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
status: 'Failed'
|
||||
}
|
||||
})
|
||||
successfulDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
status: 'Sent'
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Successfully fetched ${logs.length} logs from database`);
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error("Database query error:", dbError.message);
|
||||
console.error("Stack trace:", dbError.stack);
|
||||
|
||||
// Uncommenting the mock data code below will revert to using mock data
|
||||
// If you want to see real errors, keep this commented out
|
||||
|
||||
/*
|
||||
// If the database table doesn't exist or there's another error, use mock data
|
||||
logs = generateMockLogs(limit);
|
||||
total = 25; // Mock total count
|
||||
totalLogs = 25;
|
||||
failedDeliveries = 3;
|
||||
successfulDeliveries = 20;
|
||||
*/
|
||||
|
||||
throw dbError; // Re-throw to see actual error in response
|
||||
}
|
||||
} catch (prismaError) {
|
||||
console.error("Prisma error:", prismaError.message);
|
||||
console.error("Stack trace:", prismaError.stack);
|
||||
|
||||
// Uncommenting the mock data code below will revert to using mock data
|
||||
// If you want to see real errors, keep this commented out
|
||||
|
||||
/*
|
||||
// If there's an issue with Prisma itself, use mock data
|
||||
logs = generateMockLogs(10);
|
||||
total = 25; // Mock total count
|
||||
totalLogs = 25;
|
||||
failedDeliveries = 3;
|
||||
successfulDeliveries = 20;
|
||||
*/
|
||||
|
||||
throw prismaError; // Re-throw to see actual error in response
|
||||
}
|
||||
|
||||
// Calculate success rate
|
||||
const successRate = totalLogs > 0
|
||||
? Math.round((successfulDeliveries / totalLogs) * 100)
|
||||
: 0
|
||||
|
||||
const page = parseInt(query.page) || 1
|
||||
const limit = parseInt(query.limit) || 10
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
},
|
||||
summary: {
|
||||
totalLogs,
|
||||
failedDeliveries,
|
||||
successfulDeliveries,
|
||||
successRate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching notification logs:', error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Failed to fetch notification logs',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Helper function to generate mock logs for testing
|
||||
function generateMockLogs(count = 10) {
|
||||
const actions = ['Notification Created', 'Notification Sent', 'Delivery Attempted', 'Notification Opened'];
|
||||
const statuses = ['Sent', 'Failed', 'Opened', 'Queued'];
|
||||
const channels = ['Email', 'SMS', 'Push Notification', 'Webhook'];
|
||||
const actors = ['System', 'Admin', 'API', 'Scheduler'];
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
const action = actions[Math.floor(Math.random() * actions.length)];
|
||||
const created_at = new Date();
|
||||
created_at.setHours(created_at.getHours() - Math.floor(Math.random() * 72)); // Random time in last 72 hours
|
||||
|
||||
return {
|
||||
id: `mock-${i+1}-${Date.now()}`.substring(0, 36),
|
||||
notification_id: `notif-${i+1}-${Date.now()}`.substring(0, 36),
|
||||
action,
|
||||
actor: actors[Math.floor(Math.random() * actors.length)],
|
||||
actor_id: `user-${i+1}`,
|
||||
channel_type: channels[Math.floor(Math.random() * channels.length)],
|
||||
status,
|
||||
details: `${action} via ${channels[Math.floor(Math.random() * channels.length)]}`,
|
||||
source_ip: `192.168.1.${i+1}`,
|
||||
error_code: status === 'Failed' ? 'ERR_DELIVERY_FAILED' : null,
|
||||
error_message: status === 'Failed' ? 'Failed to deliver notification' : null,
|
||||
created_at
|
||||
};
|
||||
});
|
||||
}
|
||||
260
server/api/notifications/logs/monitoring.get.js
Normal file
260
server/api/notifications/logs/monitoring.get.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import prisma from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const user = event.context.user;
|
||||
if (!user || !user.userID) {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: { success: false, message: 'Unauthorized' }
|
||||
}
|
||||
}
|
||||
|
||||
// Get system status metrics
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now)
|
||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
||||
|
||||
// Get total sent in last 24 hours
|
||||
const totalSent = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: oneDayAgo,
|
||||
lte: now
|
||||
},
|
||||
status: 'Sent'
|
||||
}
|
||||
})
|
||||
|
||||
// Get success rate in last 24 hours
|
||||
const totalAttempted = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: oneDayAgo,
|
||||
lte: now
|
||||
},
|
||||
action: {
|
||||
in: ['Notification Sent', 'Delivery Attempted']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const successfulDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: oneDayAgo,
|
||||
lte: now
|
||||
},
|
||||
status: 'Sent'
|
||||
}
|
||||
})
|
||||
|
||||
const successRate = totalAttempted > 0
|
||||
? ((successfulDeliveries / totalAttempted) * 100).toFixed(2)
|
||||
: 0
|
||||
|
||||
// Get error rate in last 24 hours
|
||||
const failedDeliveries = await prisma.notification_logs.count({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: oneDayAgo,
|
||||
lte: now
|
||||
},
|
||||
status: 'Failed'
|
||||
}
|
||||
})
|
||||
|
||||
const errorRate = totalAttempted > 0
|
||||
? ((failedDeliveries / totalAttempted) * 100).toFixed(2)
|
||||
: 0
|
||||
|
||||
// Get average response time (mock data since we need actual measurements)
|
||||
const avgResponseTime = "145"
|
||||
|
||||
// Calculate status based on metrics
|
||||
const getSystemStatus = (metric, thresholds) => {
|
||||
if (metric <= thresholds.healthy) return 'healthy'
|
||||
if (metric <= thresholds.warning) return 'warning'
|
||||
return 'critical'
|
||||
}
|
||||
|
||||
// Get queue status
|
||||
const queueStatus = await getQueueStatus(prisma)
|
||||
|
||||
// Get recent activity
|
||||
const recentActivity = await getRecentActivity(prisma)
|
||||
|
||||
// Get performance metrics
|
||||
const performanceMetrics = {
|
||||
cpu: 23, // Mock data
|
||||
memory: 67, // Mock data
|
||||
queueLoad: calculateQueueLoad(queueStatus)
|
||||
}
|
||||
|
||||
// Get error alerts (mock data)
|
||||
const errorAlerts = await getErrorAlerts(prisma, oneDayAgo, now)
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
success: true,
|
||||
data: {
|
||||
systemStatus: [
|
||||
{
|
||||
title: "System Health",
|
||||
value: errorRate < 1 ? "Healthy" : errorRate < 5 ? "Warning" : "Critical",
|
||||
icon: "ic:outline-favorite",
|
||||
status: getSystemStatus(errorRate, { healthy: 1, warning: 5 })
|
||||
},
|
||||
{
|
||||
title: "Throughput",
|
||||
value: `${Math.round(totalSent / 24)}/hr`,
|
||||
icon: "ic:outline-speed",
|
||||
status: 'healthy' // Simplified for now
|
||||
},
|
||||
{
|
||||
title: "Error Rate",
|
||||
value: `${errorRate}%`,
|
||||
icon: "ic:outline-error-outline",
|
||||
status: getSystemStatus(errorRate, { healthy: 1, warning: 5 })
|
||||
},
|
||||
{
|
||||
title: "Response Time",
|
||||
value: `${avgResponseTime}ms`,
|
||||
icon: "ic:outline-timer",
|
||||
status: getSystemStatus(parseFloat(avgResponseTime), { healthy: 100, warning: 200 })
|
||||
}
|
||||
],
|
||||
performanceMetrics,
|
||||
queueStatus,
|
||||
recentActivity,
|
||||
errorAlerts
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching monitoring data:', error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: { success: false, message: 'Failed to fetch monitoring data', error: error.message }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
async function getQueueStatus(prisma) {
|
||||
const channels = ['Email', 'SMS', 'Push Notification', 'Webhook']
|
||||
|
||||
// In a real implementation, this would query the notification_queue table
|
||||
// For now, we'll use mock data
|
||||
return [
|
||||
{
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function calculateQueueLoad(queueStatus) {
|
||||
// Calculate average utilization across all queues
|
||||
const totalUtilization = queueStatus.reduce((sum, queue) => sum + queue.utilization, 0)
|
||||
return Math.round(totalUtilization / queueStatus.length)
|
||||
}
|
||||
|
||||
async function getRecentActivity(prisma) {
|
||||
// Get the most recent 5 log entries
|
||||
const recentLogs = await prisma.notification_logs.findMany({
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
},
|
||||
take: 5
|
||||
})
|
||||
|
||||
// Format for the frontend
|
||||
return recentLogs.map(log => {
|
||||
const timeAgo = formatTimeAgo(log.created_at)
|
||||
|
||||
return {
|
||||
action: log.action,
|
||||
description: log.details || 'No details provided',
|
||||
status: log.status ? log.status.toLowerCase() : 'unknown',
|
||||
time: timeAgo,
|
||||
source: log.channel_type || 'System'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function 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`
|
||||
}
|
||||
|
||||
async function getErrorAlerts(prisma, startDate, endDate) {
|
||||
// Get recent errors
|
||||
const recentErrors = await prisma.notification_logs.findMany({
|
||||
where: {
|
||||
created_at: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
},
|
||||
status: 'Failed',
|
||||
error_message: {
|
||||
not: null
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
},
|
||||
take: 3
|
||||
})
|
||||
|
||||
// Format for the frontend
|
||||
return recentErrors.map(error => {
|
||||
const timeAgo = formatTimeAgo(error.created_at)
|
||||
|
||||
// Determine severity based on error code or other factors
|
||||
let severity = 'warning'
|
||||
if (error.error_code && error.error_code.startsWith('CRIT')) {
|
||||
severity = 'critical'
|
||||
}
|
||||
|
||||
return {
|
||||
title: error.action || 'Error Detected',
|
||||
description: error.error_message || 'Unknown error occurred',
|
||||
timestamp: timeAgo,
|
||||
component: error.channel_type || 'System',
|
||||
severity
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user