From 38eb2a11d51763fb8ccd6724675799d1bd413554 Mon Sep 17 00:00:00 2001 From: debu-2 Date: Fri, 2 Oct 2026 11:41:33 +0530 Subject: [PATCH] Fix LogsPage missing after clone by correcting frontend .gitignore --- frontend/.gitignore | 4 +- frontend/src/features/logs/LogsPage.tsx | 277 ++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/logs/LogsPage.tsx diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..60bb8d2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,5 +1,5 @@ -# Logs -logs +# Logs (root-level only — do not use bare "logs" or src/features/logs is ignored) +/logs *.log npm-debug.log* yarn-debug.log* diff --git a/frontend/src/features/logs/LogsPage.tsx b/frontend/src/features/logs/LogsPage.tsx new file mode 100644 index 0000000..01c7a20 --- /dev/null +++ b/frontend/src/features/logs/LogsPage.tsx @@ -0,0 +1,277 @@ +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { logsApi, type LogType, type LogsResponse } from '../../lib/api' + +const LOG_TYPES: Array<{ value: LogType; label: string }> = [ + { value: 'outbound', label: 'Outbound' }, + { value: 'inbound', label: 'Inbound' }, + { value: 'sms', label: 'SMS' }, + { value: 'otp', label: 'OTP' }, +] + +const PAGE_SIZE = 10 +const DEFAULT_RANGE_DAYS = 7 +const MAX_RANGE_DAYS = 31 + +function formatDateInput(date: Date) { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +function startOfDay(date: Date) { + const copy = new Date(date) + copy.setHours(0, 0, 0, 0) + return copy +} + +function endOfDay(date: Date) { + const copy = new Date(date) + copy.setHours(23, 59, 59, 999) + return copy +} + +function formatTimestamp(value: string) { + return new Date(value).toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +function formatPhone(value: string) { + const digits = value.replace(/\D/g, '') + if (digits.length === 12 && digits.startsWith('91')) { + return `+91 ${digits.slice(2, 7)} ${digits.slice(7)}` + } + return value +} + +function statusClass(status: string) { + if (['sent', 'delivered', 'accepted', 'ended', 'active'].includes(status)) return 'success' + if (['failed', 'no_answer'].includes(status)) return 'failed' + return 'pending' +} + +function defaultRange() { + const to = new Date() + const from = new Date() + from.setDate(from.getDate() - DEFAULT_RANGE_DAYS) + return { from: formatDateInput(from), to: formatDateInput(to) } +} + +export function LogsPage() { + const initialRange = useMemo(() => defaultRange(), []) + const [logType, setLogType] = useState('outbound') + const [fromDate, setFromDate] = useState(initialRange.from) + const [toDate, setToDate] = useState(initialRange.to) + const [page, setPage] = useState(1) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + const rangeError = useMemo(() => { + const from = startOfDay(new Date(fromDate)) + const to = endOfDay(new Date(toDate)) + if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) { + return 'Enter valid dates.' + } + if (from > to) { + return 'Start date must be before end date.' + } + const spanDays = Math.ceil((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24)) + if (spanDays > MAX_RANGE_DAYS) { + return `Maximum range is ${MAX_RANGE_DAYS} days.` + } + return '' + }, [fromDate, toDate]) + + const loadLogs = useCallback(async () => { + if (rangeError) { + setLoading(false) + return + } + setLoading(true) + setError('') + try { + const fromIso = startOfDay(new Date(fromDate)).toISOString() + const toIso = endOfDay(new Date(toDate)).toISOString() + const response = await logsApi.list({ + type: logType, + from: fromIso, + to: toIso, + page, + }) + setData(response) + } catch (reason) { + setData(null) + setError(reason instanceof Error ? reason.message : 'Unable to load logs') + } finally { + setLoading(false) + } + }, [fromDate, logType, page, rangeError, toDate]) + + useEffect(() => { + loadLogs() + }, [loadLogs]) + + function changeType(nextType: LogType) { + setLogType(nextType) + setPage(1) + } + + function applyDateChange(kind: 'from' | 'to', value: string) { + if (kind === 'from') setFromDate(value) + else setToDate(value) + setPage(1) + } + + const columns = useMemo(() => { + switch (logType) { + case 'sms': + return ['From', 'To', 'Template', 'Status', 'Time'] + case 'otp': + return ['To', 'OTP type', 'Project', 'Status', 'Time'] + case 'inbound': + return ['From', 'To', 'Status', 'Time'] + default: + return ['From', 'To', 'Socket', 'Status', 'Time'] + } + }, [logType]) + + const rows = data?.items ?? [] + const totalPages = data?.total_pages ?? 0 + + return ( +
+
+ Test & observe +

Logs

+

Browse outbound and inbound calls, SMS sends, and OTP deliveries. Default view covers the last week, up to one month.

+
+ +
+
+ + + + + +
+ +
+ {PAGE_SIZE} records per page + {data && ( + + Showing page {data.page} of {Math.max(totalPages, 1)} · {data.total} total + + )} +
+
+ + {(rangeError || error) && ( +

{rangeError || error}

+ )} + +
+ {loading ? ( +

Loading logs…

+ ) : rows.length === 0 ? ( +

No {logType} logs in this time range.

+ ) : ( +
+
+ {columns.map((column) => {column})} +
+ {rows.map((row) => ( +
+ {logType === 'sms' && ( + <> + {row.from} + {formatPhone(row.to ?? '')} + {row.template} + {row.status} + {formatTimestamp(row.time)} + + )} + {logType === 'otp' && ( + <> + {formatPhone(row.to ?? '')} + {row.otp_type?.replace(/_/g, ' ')} + {row.project} + {row.status} + {formatTimestamp(row.time)} + + )} + {logType === 'inbound' && ( + <> + {formatPhone(row.from ?? '')} + {formatPhone(row.to ?? '')} + {row.status} + {formatTimestamp(row.time)} + + )} + {logType === 'outbound' && ( + <> + {formatPhone(row.from ?? '')} + {formatPhone(row.to ?? '')} + {row.socket_name || '—'} + {row.status} + {formatTimestamp(row.time)} + + )} +
+ ))} +
+ )} + + {!loading && totalPages > 1 && ( +
+ + Page {page} of {totalPages} + +
+ )} +
+
+ ) +}