From 47c3f5f0cd6152376c70475c07325b88d8c099c9 Mon Sep 17 00:00:00 2001 From: Lost-Z Date: Thu, 30 Jul 2026 10:24:21 +0000 Subject: [PATCH] feat: API response time logging, delivery failure reasons, config validation, notification sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #491 Closes #493 Closes #494 Closes #495 ## #491 — Add API Response Time Logging - New ResponseTimeMiddleware (listener/src/middleware/response-time.ts) - Records start time per request via res object symbol key - Sets X-Response-Time header on every response - Logs completed requests at INFO; flags slow requests at WARN - Tracks totalRequests, slowRequests, totalResponseTimeMs, maxResponseTimeMs - slowRequestThresholdMs defaults to 1000 ms (configurable via EventsServerOptions) - Wire middleware into createEventsServer: start() at request open, finish() at 404 fallthrough - New GET /api/metrics/response-time endpoint exposes counters; ?reset=true resets them ## #493 — Add Delivery Failure Reasons - NotificationSearchService: add failureReason field to NotificationSearchResult - Populated from last_error (scheduled_notifications) and error_reason (processed_events) - Dashboard eventsApi.ts: expose failureReason on NotificationSearchResult - NotificationSearchPage: render Failure reason row in NotificationResultCard when present ## #494 — Improve Configuration Validation - New validateConfig(config) in listener/src/config.ts - Validates all critical fields: RPC URL format, port range, poll intervals, scheduler lock/batch settings, retry multiplier/delay ordering, rate-limit minimums, analytics bucket size, cleanup retention floor - Collects all violations before throwing so operators see every problem at once - Throws ConfigError with numbered list of descriptive messages - index.ts: call validateConfig(config) immediately after loadConfig() so invalid configuration blocks startup before any service initialises ## #495 — Add Notification Sorting Options - New NotificationSortOption type (newest | oldest | priority | delivery_status) added to dashboard/src/types/event.ts; sortBy field added to EventFilters - sortEvents() utility in eventData.ts implements all four sort strategies; filterEvents() accepts and applies sortBy parameter - EventFiltersBar: Sort by dropdown persists selection to localStorage via setSortBy - eventStore: setSortBy action writes to localStorage; loadPersistedSort() restores on init; selectFilteredEvents and useFilteredEvents pass sortBy through - Server-side: NotificationSearchParams.sortBy (newest | oldest | status) added; NotificationSearchService.search() applies sort before pagination - events-server: extracts sortBy query param and forwards to search service - NotificationSearchPage: Sort by dropdown (newest/oldest/status); included in filtersKey so page resets on sort change; cleared by Clear filters --- dashboard/src/components/EventFiltersBar.tsx | 24 +++ dashboard/src/hooks/useEventSelectors.ts | 4 +- .../src/pages/NotificationSearchPage.tsx | 28 ++- dashboard/src/services/eventsApi.ts | 8 + dashboard/src/store/eventStore.ts | 33 ++- dashboard/src/types/event.ts | 12 ++ dashboard/src/utils/eventData.ts | 55 ++++- listener/src/api/events-server.ts | 37 ++++ listener/src/config.ts | 199 ++++++++++++++++++ listener/src/index.ts | 5 +- listener/src/middleware/response-time.ts | 149 +++++++++++++ .../services/notification-search-service.ts | 45 +++- 12 files changed, 589 insertions(+), 10 deletions(-) create mode 100644 listener/src/middleware/response-time.ts diff --git a/dashboard/src/components/EventFiltersBar.tsx b/dashboard/src/components/EventFiltersBar.tsx index a598cf6..8d6f91a 100644 --- a/dashboard/src/components/EventFiltersBar.tsx +++ b/dashboard/src/components/EventFiltersBar.tsx @@ -2,6 +2,14 @@ import { memo } from 'react'; import { useEventStore } from '../store/eventStore'; import { useEventCount, useEventFilters, useFilterOptions } from '../hooks/useEventSelectors'; import { SearchAutocomplete } from './SearchAutocomplete'; +import type { NotificationSortOption } from '../types/event'; + +const SORT_OPTIONS: { value: NotificationSortOption; label: string }[] = [ + { value: 'newest', label: 'Newest first' }, + { value: 'oldest', label: 'Oldest first' }, + { value: 'priority', label: 'Priority (on-chain order)' }, + { value: 'delivery_status', label: 'Delivery status' }, +]; export const EventFiltersBar = memo(function EventFiltersBar() { const filters = useEventFilters(); @@ -10,6 +18,7 @@ export const EventFiltersBar = memo(function EventFiltersBar() { const setSearch = useEventStore((state) => state.setSearch); const setContractFilter = useEventStore((state) => state.setContractFilter); const setEventTypeFilter = useEventStore((state) => state.setEventTypeFilter); + const setSortBy = useEventStore((state) => state.setSortBy); return (
@@ -53,6 +62,21 @@ export const EventFiltersBar = memo(function EventFiltersBar() { + {/* Sort control (#495) */} +
+ + +
+

{totalCount.toLocaleString()} events loaded

diff --git a/dashboard/src/hooks/useEventSelectors.ts b/dashboard/src/hooks/useEventSelectors.ts index ba53e31..bac4ed5 100644 --- a/dashboard/src/hooks/useEventSelectors.ts +++ b/dashboard/src/hooks/useEventSelectors.ts @@ -16,7 +16,9 @@ export function useFilteredEvents() { filters.eventType, filters.status, filters.dateFrom, - filters.dateTo + filters.dateTo, + filters.txHash, + filters.sortBy ?? 'newest', ), [events, filters] ); diff --git a/dashboard/src/pages/NotificationSearchPage.tsx b/dashboard/src/pages/NotificationSearchPage.tsx index 606fb06..6b36113 100644 --- a/dashboard/src/pages/NotificationSearchPage.tsx +++ b/dashboard/src/pages/NotificationSearchPage.tsx @@ -50,6 +50,7 @@ export function NotificationSearchPage() { const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [page, setPage] = useState(1); + const [sortBy, setSortBy] = useState<'newest' | 'oldest' | 'status'>('newest'); const [response, setResponse] = useState(null); const [loading, setLoading] = useState(false); @@ -86,6 +87,7 @@ export function NotificationSearchPage() { type: type || undefined, startDate: dateFrom || undefined, endDate: dateTo || undefined, + sortBy, }; }, [ debouncedQuery, @@ -96,6 +98,7 @@ export function NotificationSearchPage() { type, dateFrom, dateTo, + sortBy, ]); const runSearch = useCallback(async () => { @@ -127,7 +130,7 @@ export function NotificationSearchPage() { }, [currentFilters, page, hasParams]); // Re-run search whenever debounced params change; reset page when filters change - const filtersKey = `${debouncedQuery}|${debouncedSender}|${debouncedTxHash}|${debouncedEventId}|${status}|${type}|${dateFrom}|${dateTo}`; + const filtersKey = `${debouncedQuery}|${debouncedSender}|${debouncedTxHash}|${debouncedEventId}|${status}|${type}|${dateFrom}|${dateTo}|${sortBy}`; const prevFiltersRef = useRef(filtersKey); useEffect(() => { if (filtersKey !== prevFiltersRef.current) { @@ -149,6 +152,7 @@ export function NotificationSearchPage() { setType(''); setDateFrom(''); setDateTo(''); + setSortBy('newest'); setPage(1); setResponse(null); setError(null); @@ -274,6 +278,22 @@ export function NotificationSearchPage() { + {/* Sort control (#495) */} +
+ + +
+