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:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View 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 }
}
}
})

View 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`
}

View 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
};
});
}

View 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
}
})
}