@@ -9,7 +9,11 @@ type Connection = {
9
9
starttime : string
10
10
}
11
11
12
+ type SortKey = 'id' | 'src' | 'path' | 'protocols' | 'bytesrx' | 'bytestx' | 'starttime'
13
+
12
14
const previousData : Record < string , { bytesRx : number ; bytesTx : number ; timestamp : number } > = { }
15
+ const tbody = document . getElementById ( 'connection-tbody' ) as HTMLTableSectionElement
16
+ let currentSort : { key : SortKey , dir : 'asc' | 'desc' } = { key : 'starttime' , dir : 'desc' }
13
17
14
18
function formatSpeed ( speed : number ) : string {
15
19
if ( ! isFinite ( speed ) ) return '0 B/s'
@@ -55,82 +59,107 @@ async function killConnection(cid: string) {
55
59
}
56
60
}
57
61
58
- async function fetchData ( ) {
62
+ async function updateUptime ( ) {
59
63
try {
60
- const resp = await fetch ( '/api/v1/tcp/connections' )
61
- const json = await resp . json ( ) as Record < string , Connection >
62
- displayData ( json )
63
- } catch ( e ) {
64
- console . error ( 'Error:' , e )
64
+ const resp = await fetch ( '/api/v1/uptime' , { redirect : 'error' } )
65
+ if ( ! resp . ok ) throw new Error ( 'bad' )
66
+ const text = await resp . text ( )
67
+ const el = document . getElementById ( 'uptime' )
68
+ if ( el ) { el . textContent = text ; el . classList . remove ( 'disconnected' ) }
69
+ } catch {
70
+ const el = document . getElementById ( 'uptime' )
71
+ if ( el ) { el . textContent = 'Disconnected' ; el . classList . add ( 'disconnected' ) }
65
72
}
66
73
}
67
74
68
- function displayData ( connections : Record < string , Connection > ) {
69
- const ul = document . getElementById ( 'connection-list' ) as HTMLUListElement
70
- ul . innerHTML = ''
75
+ function compare ( a : any , b : any , key : SortKey ) : number {
76
+ if ( key === 'bytesrx' || key === 'bytestx' ) {
77
+ return Number ( a [ key ] ?? 0 ) - Number ( b [ key ] ?? 0 )
78
+ }
79
+ if ( key === 'starttime' ) {
80
+ return new Date ( a [ key ] ) . getTime ( ) - new Date ( b [ key ] ) . getTime ( )
81
+ }
82
+ return String ( a [ key ] ?? '' ) . localeCompare ( String ( b [ key ] ?? '' ) )
83
+ }
84
+
85
+ function renderRows ( rows : Array < Record < string , any > > ) {
86
+ tbody . innerHTML = ''
71
87
const now = Date . now ( )
72
- Object . keys ( connections ) . forEach ( key => {
73
- const c = connections [ key ]
74
- const li = document . createElement ( 'li' )
75
- li . className = 'py-3'
76
-
77
- const title = document . createElement ( 'div' )
78
- title . className = 'text-sm font-medium text-neutral-900 dark:text-neutral-100'
79
- title . textContent = `Connection ID: ${ key } `
80
- li . appendChild ( title )
81
-
82
- const start = new Date ( c . starttime )
83
- const prev = previousData [ key ] ?? { bytesRx : c . bytesrx , bytesTx : c . bytestx , timestamp : now }
88
+ for ( const row of rows ) {
89
+ const tr = document . createElement ( 'tr' )
90
+ tr . className = 'hover:bg-neutral-50 dark:hover:bg-neutral-800/40'
91
+
92
+ const start = new Date ( row . starttime )
93
+ const prev = previousData [ row . id ] ?? { bytesRx : row . bytesrx , bytesTx : row . bytestx , timestamp : now }
84
94
const dt = Math . max ( 0.001 , ( now - prev . timestamp ) / 1000 )
85
- const speedRx = ( c . bytesrx - prev . bytesRx ) / dt
86
- const speedTx = ( c . bytestx - prev . bytesTx ) / dt
87
-
88
- const meta = document . createElement ( 'div' )
89
- meta . className = 'mt-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-1 text-xs text-neutral-700 dark:text-neutral-300 font-mono'
90
- meta . innerHTML = `
91
- <div>BytesRx: ${ c . bytesrx } </div>
92
- <div>BytesTx: ${ c . bytestx } </div>
93
- <div>Path: ${ c . path } </div>
94
- <div>Protocols: ${ c . protocols } </div>
95
- <div>Source: ${ c . src } </div>
96
- <div>Start: ${ start . toLocaleString ( ) } </div>
97
- <div>SpeedRx: ${ formatSpeed ( speedRx ) } </div>
98
- <div>SpeedTx: ${ formatSpeed ( speedTx ) } </div>
99
- <div>Elapsed: ${ formatTime ( now - start . getTime ( ) ) } </div>
95
+ const speedRx = ( row . bytesrx - prev . bytesRx ) / dt
96
+ const speedTx = ( row . bytestx - prev . bytesTx ) / dt
97
+
98
+ tr . innerHTML = `
99
+ <td class="px-3 py-2 font-mono">${ row . id } </td>
100
+ <td class="px-3 py-2 font-mono text-xs">${ row . src } </td>
101
+ <td class="px-3 py-2">${ row . path } </td>
102
+ <td class="px-3 py-2">${ row . protocols } </td>
103
+ <td class="px-3 py-2">${ row . bytesrx } </td>
104
+ <td class="px-3 py-2">${ row . bytestx } </td>
105
+ <td class="px-3 py-2 font-mono text-xs">${ row . starttime } </td>
106
+ <td class="px-3 py-2 font-mono">${ formatTime ( now - start . getTime ( ) ) } </td>
107
+ <td class="px-3 py-2 font-mono text-xs">${ formatSpeed ( speedRx ) } </td>
108
+ <td class="px-3 py-2 font-mono text-xs">${ formatSpeed ( speedTx ) } </td>
109
+ <td class="px-3 py-2">
110
+ <button data-cid="${ row . id } " class="px-2 py-1 rounded-md bg-red-600 text-white hover:bg-red-700 text-xs">Kill</button>
111
+ </td>
100
112
`
101
- li . appendChild ( meta )
113
+ tbody . appendChild ( tr )
102
114
103
- previousData [ key ] = { bytesRx : c . bytesrx , bytesTx : c . bytestx , timestamp : now }
115
+ previousData [ row . id ] = { bytesRx : row . bytesrx , bytesTx : row . bytestx , timestamp : now }
116
+ }
117
+ }
104
118
105
- const actions = document . createElement ( 'div' )
106
- actions . className = 'mt-2'
107
- const killBtn = document . createElement ( 'button' )
108
- killBtn . className = 'px-3 py-1.5 rounded-md bg-red-600 text-white hover:bg-red-700'
109
- killBtn . textContent = 'Kill'
110
- killBtn . addEventListener ( 'click' , ( ) => killConnection ( key ) )
111
- actions . appendChild ( killBtn )
112
- li . appendChild ( actions )
119
+ async function fetchData ( ) {
120
+ try {
121
+ const resp = await fetch ( '/api/v1/tcp/connections' )
122
+ const json = await resp . json ( ) as Record < string , Connection >
123
+ const rows = Object . keys ( json ) . map ( id => ( { id, ...( json [ id ] || { } ) } ) )
124
+ rows . sort ( ( a , b ) => {
125
+ const cmp = compare ( a , b , currentSort . key )
126
+ return currentSort . dir === 'asc' ? cmp : - cmp
127
+ } )
128
+ renderRows ( rows )
129
+ } catch ( e ) {
130
+ console . error ( 'Error:' , e )
131
+ }
132
+ }
113
133
114
- ul . appendChild ( li )
134
+ function setupSorting ( ) {
135
+ const heads = document . querySelectorAll ( 'thead th[data-key]' )
136
+ heads . forEach ( th => {
137
+ th . addEventListener ( 'click' , ( ) => {
138
+ const key = ( th as HTMLElement ) . dataset . key as SortKey
139
+ if ( ! key ) return
140
+ if ( currentSort . key === key ) {
141
+ currentSort = { key, dir : currentSort . dir === 'asc' ? 'desc' : 'asc' }
142
+ } else {
143
+ currentSort = { key, dir : 'asc' }
144
+ }
145
+ void fetchData ( )
146
+ } )
115
147
} )
116
148
}
117
149
118
- async function updateUptime ( ) {
119
- try {
120
- const resp = await fetch ( '/api/v1/uptime' , { redirect : 'error' } )
121
- if ( ! resp . ok ) throw new Error ( 'bad' )
122
- const text = await resp . text ( )
123
- const el = document . getElementById ( 'uptime' )
124
- if ( el ) { el . textContent = text ; el . classList . remove ( 'disconnected' ) }
125
- } catch {
126
- const el = document . getElementById ( 'uptime' )
127
- if ( el ) { el . textContent = 'Disconnected' ; el . classList . add ( 'disconnected' ) }
128
- }
150
+ function setupActions ( ) {
151
+ tbody . addEventListener ( 'click' , ( ev ) => {
152
+ const target = ev . target as HTMLElement
153
+ const btn = target . closest ( 'button[data-cid]' ) as HTMLButtonElement | null
154
+ if ( btn && btn . dataset . cid ) {
155
+ void killConnection ( btn . dataset . cid )
156
+ }
157
+ } )
129
158
}
130
159
160
+ setupSorting ( )
161
+ setupActions ( )
131
162
setInterval ( fetchData , 1000 )
132
- fetchData ( )
133
- updateUptime ( )
163
+ void fetchData ( )
164
+ void updateUptime ( )
134
165
setInterval ( updateUptime , 1000 )
135
-
136
-
0 commit comments