From 63733651d59ba2f45c3aa1599179433bfbf2c4fa Mon Sep 17 00:00:00 2001 From: isra el Date: Sun, 21 Sep 2025 23:43:32 +0300 Subject: [PATCH 1/3] chore(api): improve webhook delivery history aggregation query --- api/src/webhook/webhook.service.ts | 65 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/api/src/webhook/webhook.service.ts b/api/src/webhook/webhook.service.ts index ffb7705..317aec5 100644 --- a/api/src/webhook/webhook.service.ts +++ b/api/src/webhook/webhook.service.ts @@ -84,27 +84,6 @@ export class WebhookService { matchStage.createdAt = { $gte: new Date(start), $lte: new Date(end) } } - if (status) { - switch (status) { - case 'delivered': - matchStage.deliveredAt = { $ne: null } - break - case 'failed': - matchStage.deliveryAttemptAbortedAt = { $ne: null } - break - case 'retrying': - matchStage.deliveredAt = null - matchStage.deliveryAttemptAbortedAt = null - matchStage.deliveryAttemptCount = { $gt: 0 } - matchStage.nextDeliveryAttemptAt = { $ne: null } - break - case 'pending': - matchStage.deliveredAt = null - matchStage.deliveryAttemptAbortedAt = null - matchStage.deliveryAttemptCount = 0 - break - } - } const pageNum = Math.max(1, Number.parseInt(page.toString()) || 1) const limitNum = Math.max(1, Number.parseInt(limit.toString()) || 10) @@ -144,8 +123,52 @@ export class WebhookService { preserveNullAndEmptyArrays: true, }, }, + { + $addFields: { + computedStatus: { + $cond: { + if: { $ne: ['$deliveredAt', null] }, + then: 'delivered', + else: { + $cond: { + if: { + $or: [ + { $ne: ['$deliveryAttemptAbortedAt', null] }, + { $gte: ['$deliveryAttemptCount', 10] } + ] + }, + then: 'failed', + else: { + $cond: { + if: { + $and: [ + { $eq: ['$deliveredAt', null] }, + { $eq: ['$deliveryAttemptAbortedAt', null] }, + { $gt: ['$deliveryAttemptCount', 0] }, + { $lt: ['$deliveryAttemptCount', 10] } + ] + }, + then: 'retrying', + else: 'pending' + } + } + } + } + } + } + } + }, ] + // Apply status filter + if (status) { + commonPipeline.push({ + $match: { + computedStatus: status + } + }) + } + if (deviceId) { commonPipeline.push({ $match: { From 4f64880cf8fd25389e4babf7f1ac506999d8b3b0 Mon Sep 17 00:00:00 2001 From: isra el Date: Sun, 21 Sep 2025 23:47:53 +0300 Subject: [PATCH 2/3] ui(web): improve webhook delivery history page ui --- .../(components)/webhooks-history.tsx | 216 ++++++++++-------- .../webhooks/webhooks-section.tsx | 2 +- .../webhooks/(components)/data-table.tsx | 10 +- ...ms-modal.tsx => webhook-payload-modal.tsx} | 76 ++++-- .../webhooks/(components)/webhook-table.tsx | 16 +- web/app/(app)/dashboard/webhooks/page.tsx | 2 +- 6 files changed, 204 insertions(+), 118 deletions(-) rename web/app/(app)/dashboard/webhooks/(components)/{sms-modal.tsx => webhook-payload-modal.tsx} (54%) diff --git a/web/app/(app)/dashboard/(components)/webhooks-history.tsx b/web/app/(app)/dashboard/(components)/webhooks-history.tsx index adff581..e55a678 100644 --- a/web/app/(app)/dashboard/(components)/webhooks-history.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks-history.tsx @@ -53,9 +53,9 @@ const WebhooksHistory = () => { .then((res) => res.data), }) - const [currentDevice, setCurrentDevice] = useState('') - const [eventType, setEventType] = useState('MESSAGE_RECEIVED') - const [status, setStatus] = useState('delivered') + const [currentDevice, setCurrentDevice] = useState('all') + const [eventType, setEventType] = useState('all') + const [status, setStatus] = useState('all') const [dateRange, setDateRange] = useState('90') const [openCal, setOpenCal] = useState(false) const [dateQuery, setDateQuery] = useState<{ start: string; end: string }>({ @@ -67,10 +67,9 @@ const WebhooksHistory = () => { const [isLoading, setIsLoading] = useState(false) useEffect(() => { - if (devices?.data?.length) { - setCurrentDevice(devices?.data?.[0]?._id) + if (devices?.data?.length && currentDevice === 'all') { } - }, [devices]) + }, [devices, currentDevice]) const { data: webhookNotifications, @@ -86,11 +85,11 @@ const WebhooksHistory = () => { currentDevice, status, ], - enabled: !!currentDevice, + enabled: true, queryFn: () => httpBrowserClient .get( - `${ApiEndpoints.gateway.getWebhookNotifications()}?eventType=${eventType}&page=${page}&limit=${limit}&status=${status}&start=${ + `${ApiEndpoints.gateway.getWebhookNotifications()}?eventType=${eventType === 'all' ? '' : eventType}&page=${page}&limit=${limit}&status=${status === 'all' ? '' : status}&start=${ dateQuery.start }&end=${dateQuery.end}&deviceId=${ currentDevice === 'all' ? '' : currentDevice @@ -125,6 +124,15 @@ const WebhooksHistory = () => { case '90': start.setDate(end.getDate() - 90) break + case '90_months': + start.setMonth(end.getMonth() - 3) + break + case '180': + start.setMonth(end.getMonth() - 6) + break + case '365': + start.setMonth(end.getMonth() - 12) + break case 'custom': setDateQuery({ start: '', end: '' }) setDateRange('custom') @@ -156,6 +164,9 @@ const WebhooksHistory = () => { + + All devices + {devices?.data?.map((device) => (
@@ -173,9 +184,6 @@ const WebhooksHistory = () => {
))} - - All devices -
@@ -192,6 +200,12 @@ const WebhooksHistory = () => { + +
+
+ All Events +
+
@@ -218,6 +232,12 @@ const WebhooksHistory = () => { + +
+
+ All +
+
@@ -272,96 +292,104 @@ Custom should trigger a popover to set a custom date range ( two date inputs) */ Last 90 Days
+ +
+ Last 3 Months +
+
+ +
+ Last 6 Months +
+
+ +
+ Last 12 Months +
+
Custom
-
-
-
-
-
- {dateRange === 'custom' && ( - - - - - {openCal && ( - -
-
- - - setDateQuery({ - ...dateQuery, - start: e.target.value, - }) - } - /> -
-
- - - setDateQuery({ - ...dateQuery, - end: e.target.value, - }) - } - /> -
- -
-
- )} -
- )} -
+ + {/* Custom Date Range Popup */} + + +
+
+ + + setDateQuery({ + ...dateQuery, + start: e.target.value ? new Date(e.target.value).toISOString() : '', + }) + } + /> +
+
+ + + setDateQuery({ + ...dateQuery, + end: e.target.value ? new Date(e.target.value).toISOString() : '', + }) + } + /> +
+
+ + +
+
+
+
+ {isLoadingNotifications || isLoading ? ( ) : ( diff --git a/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx index b397ac9..5d992f7 100644 --- a/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx @@ -130,7 +130,7 @@ export default function WebhooksSection() { className='w-full sm:w-auto' > - Webhook Notification + Notification Deliveries
diff --git a/web/app/(app)/dashboard/webhooks/(components)/data-table.tsx b/web/app/(app)/dashboard/webhooks/(components)/data-table.tsx index 52e3ef5..974f025 100644 --- a/web/app/(app)/dashboard/webhooks/(components)/data-table.tsx +++ b/web/app/(app)/dashboard/webhooks/(components)/data-table.tsx @@ -20,7 +20,7 @@ import { } from '@/components/ui/table' import React from 'react' import { Skeleton } from '@/components/ui/skeleton' -import { SmsModal } from './sms-modal' // Import your modal +import { WebhookPayloadModal } from './webhook-payload-modal' // Import your modal interface DataTableProps { columns: ColumnDef[] @@ -39,6 +39,7 @@ export function DataTable({ [] ) const [selectedSms, setSelectedSms] = React.useState(null) + const [selectedPayload, setSelectedPayload] = React.useState(null) const [isModalOpen, setIsModalOpen] = React.useState(false) const table = useReactTable({ @@ -57,6 +58,7 @@ export function DataTable({ // Assuming your row data has smsData property if (row.original.smsData) { setSelectedSms(row.original.smsData) + setSelectedPayload(row.original.payload) setIsModalOpen(true) } } @@ -64,6 +66,7 @@ export function DataTable({ const handleCloseModal = () => { setIsModalOpen(false) setSelectedSms(null) + setSelectedPayload(null) } return ( @@ -133,11 +136,12 @@ export function DataTable({
- {/* SMS Modal */} -
) diff --git a/web/app/(app)/dashboard/webhooks/(components)/sms-modal.tsx b/web/app/(app)/dashboard/webhooks/(components)/webhook-payload-modal.tsx similarity index 54% rename from web/app/(app)/dashboard/webhooks/(components)/sms-modal.tsx rename to web/app/(app)/dashboard/webhooks/(components)/webhook-payload-modal.tsx index 0c76043..0ad515c 100644 --- a/web/app/(app)/dashboard/webhooks/(components)/sms-modal.tsx +++ b/web/app/(app)/dashboard/webhooks/(components)/webhook-payload-modal.tsx @@ -1,4 +1,3 @@ -// components/sms-modal.tsx 'use client' import { @@ -9,13 +8,9 @@ import { } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { - X, - Calendar, - MessageSquare, - Phone, - CreditCard, - Bell, + Copy, } from 'lucide-react' +import { useToast } from '@/hooks/use-toast' interface SmsData { _id: string @@ -31,13 +26,16 @@ interface SmsData { __v: number } -interface SmsModalProps { +interface WebhookPayloadModalProps { isOpen: boolean onClose: () => void smsData: SmsData | null + payload?: any } -export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) { +export function WebhookPayloadModal({ isOpen, onClose, smsData, payload }: WebhookPayloadModalProps) { + const { toast } = useToast() + if (!smsData) return null const formatDate = (dateString: string) => { @@ -51,23 +49,64 @@ export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) { }) } + const copyPayloadToClipboard = async () => { + if (!payload) return + try { + const jsonString = JSON.stringify(payload, null, 2) + await navigator.clipboard.writeText(jsonString) + toast({ + title: "Copied!", + description: "Payload copied to clipboard", + duration: 2000, + }) + } catch (err) { + console.error('Failed to copy payload:', err) + // Fallback for older browsers + const textArea = document.createElement('textarea') + textArea.value = JSON.stringify(payload, null, 2) + document.body.appendChild(textArea) + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + toast({ + title: "Copied!", + description: "Payload copied to clipboard", + duration: 2000, + }) + } + } + return ( - + Webhook Notification -

- Message Content -

-
- {/* Message Content */} -
-

{smsData.message}

-
+
+ {payload && ( +
+
+

Payload

+ +
+
+
+                  {JSON.stringify(payload, null, 2)}
+                
+
+
+ )}
{/* Sender Information */} @@ -95,6 +134,7 @@ export function SmsModal({ isOpen, onClose, smsData }: SmsModalProps) { {smsData.status}

+

Created At diff --git a/web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx b/web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx index ed36161..7baf605 100644 --- a/web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx +++ b/web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button' import { DataTable } from './data-table' import type { ColumnDef } from '@tanstack/react-table' import { format } from 'date-fns' +import { Eye } from 'lucide-react' interface ProductClientProps { data: ProductColumns[] @@ -20,8 +21,11 @@ export type ProductColumns = { } createdAt?: string status: string + computedStatus?: string + payload?: any } + export const columns: ColumnDef[] = [ { accessorKey: 'event', @@ -59,6 +63,15 @@ export const columns: ColumnDef[] = [ accessorKey: 'createdAt', header: 'Created At', }, + { + id: 'actions', + header: 'Actions', + cell: ({ row }) => ( + + ), + }, ] const formatDate = (dateString: string) => { return format(new Date(dateString), 'MMM dd, yyyy h:mm a') @@ -74,7 +87,8 @@ const ProductClient = ({ data, isLoading, status = 'delivered' }) => { ` ${d.smsData._id}`, ], createdAt: formatDate(d.createdAt.toString()), - status, + status: d.computedStatus || status, + payload: d.payload, })) return ( diff --git a/web/app/(app)/dashboard/webhooks/page.tsx b/web/app/(app)/dashboard/webhooks/page.tsx index a2a523f..72ddb57 100644 --- a/web/app/(app)/dashboard/webhooks/page.tsx +++ b/web/app/(app)/dashboard/webhooks/page.tsx @@ -10,7 +10,7 @@ export default function MessagingPage() {

- Webhook Notifications + Webhook Notification Delivery History

From 704a8dbf95b57e0cad86b83f202390bd1e11481f Mon Sep 17 00:00:00 2001 From: isra el Date: Sun, 21 Sep 2025 23:55:35 +0300 Subject: [PATCH 3/3] chore(web): clean up unnecessary comments --- .../(app)/dashboard/(components)/webhooks-history.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web/app/(app)/dashboard/(components)/webhooks-history.tsx b/web/app/(app)/dashboard/(components)/webhooks-history.tsx index e55a678..605e801 100644 --- a/web/app/(app)/dashboard/(components)/webhooks-history.tsx +++ b/web/app/(app)/dashboard/(components)/webhooks-history.tsx @@ -272,8 +272,6 @@ const WebhooksHistory = () => { Date Range {' '}
- {/* date range ( today, last 3 days, last 7 days, last 30 days-default, last 90 days, custom) -Custom should trigger a popover to set a custom date range ( two date inputs) */}