11
22
3+ // "use client"
4+
5+ // import { useEffect, useState } from "react"
6+ // import { CheckCircle2, XCircle, Clock } from "lucide-react"
7+
8+ // type Log = {
9+ // id: string
10+ // event_id: number
11+ // status: "success" | "failed"
12+ // status_code?: number
13+ // response?: string
14+ // attempt: number
15+ // created_at: string
16+ // }
17+
18+ // /* ---------------- HELPERS ---------------- */
19+
20+
21+ // async function retryEvent(eventId: number) {
22+ // try {
23+ // await fetch(
24+ // `${process.env.NEXT_PUBLIC_API_URL}/events/${eventId}/replay`,
25+ // {
26+ // method: "POST",
27+ // credentials: "include",
28+ // }
29+ // )
30+
31+ // alert("Retry triggered")
32+ // } catch {
33+ // alert("Retry failed")
34+ // }
35+ // }
36+
37+ // function groupByEvent(logs: Log[]) {
38+ // const map: Record<number, Log[]> = {}
39+
40+ // logs.forEach((log) => {
41+ // if (!map[log.event_id]) map[log.event_id] = []
42+ // map[log.event_id].push(log)
43+ // })
44+
45+ // return Object.entries(map)
46+ // .sort((a, b) => Number(b[0]) - Number(a[0])) // latest first
47+ // .map(([eventId, items]) => ({
48+ // eventId: Number(eventId),
49+ // items: items.sort((a, b) => b.attempt - a.attempt),
50+ // }))
51+ // }
52+
53+ // function parseResponse(response?: string) {
54+ // if (!response) return null
55+
56+ // try {
57+ // return JSON.parse(response)
58+ // } catch {
59+ // return response
60+ // }
61+ // }
62+
63+ // /* ---------------- COMPONENT ---------------- */
64+
65+ // export default function TargetLogs({ targetId }: { targetId: string }) {
66+ // const [logs, setLogs] = useState<Log[]>([])
67+ // const [loading, setLoading] = useState(true)
68+
69+ // useEffect(() => {
70+ // async function fetchLogs() {
71+ // try {
72+ // const res = await fetch(
73+ // `${process.env.NEXT_PUBLIC_API_URL}/delivery-targets/${targetId}/logs`,
74+ // {
75+ // credentials: "include",
76+ // }
77+ // )
78+
79+ // const data = await res.json()
80+
81+ // setLogs((prev) => {
82+ // const next = data.items || []
83+
84+ // if (JSON.stringify(prev) === JSON.stringify(next)) {
85+ // return prev
86+ // }
87+
88+ // return next
89+ // })
90+ // } catch (err) {
91+ // console.error("Failed to fetch logs", err)
92+ // } finally {
93+ // setLoading(false)
94+ // }
95+ // }
96+
97+ // // initial fetch
98+ // fetchLogs()
99+
100+ // // interval defined as const
101+ // const interval = setInterval(fetchLogs, 5000)
102+
103+ // return () => clearInterval(interval)
104+ // }, [targetId])
105+
106+ // const grouped = groupByEvent(logs)
107+
108+ // if (loading) {
109+ // return (
110+ // <div className="p-6 text-sm text-muted-foreground">
111+ // Loading logs...
112+ // </div>
113+ // )
114+ // }
115+
116+ // return (
117+ // <div className="rounded-2xl border bg-card p-5 space-y-6">
118+
119+ // <h2 className="font-semibold text-sm">Activity Timeline</h2>
120+
121+ // {grouped.length === 0 && (
122+ // <p className="text-sm text-muted-foreground">
123+ // No activity yet
124+ // </p>
125+ // )}
126+
127+ // {grouped.map((group) => (
128+ // <div key={group.eventId} className="space-y-3">
129+
130+ // {/* Event Header */}
131+ // <div className="text-xs font-medium text-muted-foreground">
132+ // Event #{group.eventId}
133+ // </div>
134+
135+ // <div className="space-y-2 border-l pl-4">
136+
137+ // {group.items.map((log) => {
138+ // const parsed = parseResponse(log.response)
139+
140+ // return (
141+ // <div key={log.id} className="flex gap-3">
142+
143+ // {/* Timeline Dot */}
144+ // <div className="mt-1">
145+ // {log.status === "success" ? (
146+ // <CheckCircle2 className="w-4 h-4 text-emerald-500" />
147+ // ) : (
148+ // <XCircle className="w-4 h-4 text-red-500" />
149+ // )}
150+ // </div>
151+
152+ // {/* Content */}
153+ // <div className="flex-1 space-y-1">
154+
155+ // <p className="text-sm font-medium">
156+ // {log.status === "success"
157+ // ? "Delivered successfully"
158+ // : "Delivery failed" }
159+
160+ // {
161+ // log.status === "failed" && (
162+ // <button
163+ // onClick={() => retryEvent(log.event_id)}
164+ // className="text-xs px-2 py-1 rounded border hover:bg-muted"
165+ // >
166+ // Retry
167+ // </button>
168+ // )
169+ // }
170+ // </p>
171+
172+ // <div className="flex gap-3 text-xs text-muted-foreground flex-wrap">
173+
174+ // <span>Attempt {log.attempt}</span>
175+
176+ // {log.status_code && (
177+ // <span>HTTP {log.status_code}</span>
178+ // )}
179+
180+ // {parsed?.duration_ms && (
181+ // <span className="flex items-center gap-1">
182+ // <Clock className="w-3 h-3" />
183+ // {parsed.duration_ms}ms
184+ // </span>
185+ // )}
186+ // </div>
187+
188+ // {/* Response preview */}
189+ // {parsed && (
190+ // <details className="text-xs mt-1">
191+ // <summary className="cursor-pointer text-muted-foreground hover:text-foreground">
192+ // View response
193+ // </summary>
194+
195+ // <pre className="mt-2 p-2 bg-muted rounded overflow-auto text-[10px] max-h-48">
196+ // {typeof parsed === "string"
197+ // ? parsed.slice(0, 500)
198+ // : JSON.stringify(parsed, null, 2) }
199+ // </pre>
200+ // </details>
201+ // )}
202+ // </div>
203+
204+ // {/* Time */}
205+ // <div className="text-xs text-muted-foreground whitespace-nowrap">
206+ // {new Date(log.created_at).toLocaleTimeString()}
207+ // </div>
208+ // </div>
209+ // )
210+ // }) }
211+ // </div>
212+ // </div>
213+ // ))}
214+ // </div>
215+ // )
216+ // }
217+
218+
219+
220+
221+
222+
223+
3224"use client"
4225
5226import { useEffect , useState } from "react"
@@ -26,7 +247,7 @@ function groupByEvent(logs: Log[]) {
26247 } )
27248
28249 return Object . entries ( map )
29- . sort ( ( a , b ) => Number ( b [ 0 ] ) - Number ( a [ 0 ] ) ) // latest first
250+ . sort ( ( a , b ) => Number ( b [ 0 ] ) - Number ( a [ 0 ] ) )
30251 . map ( ( [ eventId , items ] ) => ( {
31252 eventId : Number ( eventId ) ,
32253 items : items . sort ( ( a , b ) => b . attempt - a . attempt ) ,
@@ -48,41 +269,61 @@ function parseResponse(response?: string) {
48269export default function TargetLogs ( { targetId } : { targetId : string } ) {
49270 const [ logs , setLogs ] = useState < Log [ ] > ( [ ] )
50271 const [ loading , setLoading ] = useState ( true )
272+ const [ retrying , setRetrying ] = useState < number | null > ( null )
273+
274+ /* ---------------- FETCH ---------------- */
275+
276+ async function fetchLogs ( ) {
277+ try {
278+ const res = await fetch (
279+ `${ process . env . NEXT_PUBLIC_API_URL } /delivery-targets/${ targetId } /logs` ,
280+ { credentials : "include" }
281+ )
282+
283+ const data = await res . json ( )
284+
285+ setLogs ( ( prev ) => {
286+ const next = data . items || [ ]
287+ if ( JSON . stringify ( prev ) === JSON . stringify ( next ) ) return prev
288+ return next
289+ } )
290+ } catch ( err ) {
291+ console . error ( "Failed to fetch logs" , err )
292+ } finally {
293+ setLoading ( false )
294+ }
295+ }
51296
52- useEffect ( ( ) => {
53- async function fetchLogs ( ) {
54- try {
55- const res = await fetch (
56- `${ process . env . NEXT_PUBLIC_API_URL } /delivery-targets/${ targetId } /logs` ,
57- {
58- credentials : "include" ,
59- }
60- )
61-
62- const data = await res . json ( )
63-
64- setLogs ( ( prev ) => {
65- const next = data . items || [ ]
66-
67- if ( JSON . stringify ( prev ) === JSON . stringify ( next ) ) {
68- return prev
69- }
70-
71- return next
72- } )
73- } catch ( err ) {
74- console . error ( "Failed to fetch logs" , err )
75- } finally {
76- setLoading ( false )
77- }
297+ /* ---------------- RETRY ---------------- */
298+
299+ async function retryEvent ( eventId : number ) {
300+ try {
301+ setRetrying ( eventId )
302+
303+ await fetch (
304+ `${ process . env . NEXT_PUBLIC_API_URL } /events/${ eventId } /replay` ,
305+ {
306+ method : "POST" ,
307+ credentials : "include" ,
308+ }
309+ )
310+
311+ // refresh logs instantly
312+ await fetchLogs ( )
313+ } catch {
314+ alert ( "Retry failed" )
315+ } finally {
316+ setRetrying ( null )
78317 }
79-
80- // initial fetch
318+ }
319+
320+ /* ---------------- EFFECT ---------------- */
321+
322+ useEffect ( ( ) => {
81323 fetchLogs ( )
82-
83- // interval defined as const
84- const interval = setInterval ( fetchLogs , 5000 )
85-
324+
325+ const interval = setInterval ( fetchLogs , 4000 ) // slightly faster
326+
86327 return ( ) => clearInterval ( interval )
87328 } , [ targetId ] )
88329
@@ -123,7 +364,7 @@ export default function TargetLogs({ targetId }: { targetId: string }) {
123364 return (
124365 < div key = { log . id } className = "flex gap-3" >
125366
126- { /* Timeline Dot */ }
367+ { /* Status Icon */ }
127368 < div className = "mt-1" >
128369 { log . status === "success" ? (
129370 < CheckCircle2 className = "w-4 h-4 text-emerald-500" />
@@ -135,12 +376,29 @@ export default function TargetLogs({ targetId }: { targetId: string }) {
135376 { /* Content */ }
136377 < div className = "flex-1 space-y-1" >
137378
138- < p className = "text-sm font-medium" >
139- { log . status === "success"
140- ? "Delivered successfully"
141- : "Delivery failed" }
142- </ p >
379+ { /* Title + Retry */ }
380+ < div className = "flex items-center gap-2" >
381+
382+ < p className = "text-sm font-medium" >
383+ { log . status === "success"
384+ ? "Delivered successfully"
385+ : "Delivery failed" }
386+ </ p >
387+
388+ { log . status === "failed" && (
389+ < button
390+ onClick = { ( ) => retryEvent ( log . event_id ) }
391+ disabled = { retrying === log . event_id }
392+ className = "text-xs px-2 py-1 rounded border hover:bg-muted"
393+ >
394+ { retrying === log . event_id
395+ ? "Retrying..."
396+ : "Retry" }
397+ </ button >
398+ ) }
399+ </ div >
143400
401+ { /* Meta */ }
144402 < div className = "flex gap-3 text-xs text-muted-foreground flex-wrap" >
145403
146404 < span > Attempt { log . attempt } </ span >
@@ -157,7 +415,7 @@ export default function TargetLogs({ targetId }: { targetId: string }) {
157415 ) }
158416 </ div >
159417
160- { /* Response preview */ }
418+ { /* Response */ }
161419 { parsed && (
162420 < details className = "text-xs mt-1" >
163421 < summary className = "cursor-pointer text-muted-foreground hover:text-foreground" >
0 commit comments