admin/monitoring: optimized for many connections
Massimo Melina committed
May 6, 2026 at 10:25 UTC
2da20e99a6c964a7991beccfb9282f98756f83d1
3 files changed
+27
-16
admin/src/MonitorPage.ts
+5
-4
@@ -36,7 +36,7 @@ export default function MonitorPage({ setTitleSide }: PageProps) {
36
function MoreInfo() {
37
const { data: status, element, reload } = useApiEx<typeof adminApis.get_status>('get_status')
38
useInterval(reload, 10_000) // status hardly change, but it can
39
- const { data: connections } = useApiEvents('get_connection_stats')
39
+ const { data: stats } = useApiEvents('get_connection_stats')
40
const [allInfo, setAllInfo] = useState(false)
41
const md = useBreakpoint('md')
42
const sm = useBreakpoint('sm')
@@ -57,7 +57,7 @@ function MoreInfo() {
57
}),
58
pair('outSpeedKb', { label: "Output", render: formatSpeedK, minWidth: '8.5em' }),
59
pair('inSpeedKb', { label: "Input", render: formatSpeedK, minWidth: '8.5em' }),
60
- (allInfo || sm) && pair('ips', { label: "IPs", title: () => "Currently connected" }),
60
+ (allInfo || sm) && pair('ips', { label: "IPs", title: () => stats && `${stats.connections.toLocaleString()} connections` }),
61
(md || allInfo && md || status?.http?.error) && pair('http', { label: "HTTP", render: port }),
62
(md || allInfo && md || status?.https?.error) && pair('https', { label: "HTTPS", render: port }),
63
(xl || allInfo) && pair('ram', { label: "RAM", render: formatBytes }),
@@ -81,7 +81,7 @@ function MoreInfo() {
81
}
82
83
function pair(k: string, { label, minWidth, render, title, onDelete }: PairOptions = {}) {
84
- let v = _.get(connections, k) ?? _.get(status, k)
84
+ let v = _.get(stats, k) ?? _.get(status, k)
85
if (v === undefined)
86
return null
87
let color: Color = undefined
@@ -118,7 +118,7 @@ function Connections() {
118
const { monitorOnlyFiles } = useSnapState()
119
const { pause, pauseButton } = usePauseButton()
120
const rows = useMemo(() =>
121
- list?.filter((x: any) => !monitorOnlyFiles || x.op).map((x: any, id: number) => ({ id, ...x })),
121
+ (!monitorOnlyFiles ? list : list?.filter((x: any) => x.op)) ?? [],
122
[!pause && list, monitorOnlyFiles]) //eslint-disable-line
123
const logAble = useBreakpoint('md')
124
const [wantLog, wantLogButton] = useToggleButton("Show log", "Hide log", v => ({
@@ -147,6 +147,7 @@ function Connections() {
147
persist: 'connections',
148
error,
149
rows,
150
+ getRowId: (row: any) => row.ip + ':' + row.port,
151
fillFlex: true,
152
noRows: monitorOnlyFiles && "No downloads/uploads at the moment",
153
actionsHeader: pauseButton,
src/api.monitor.ts
+6
-12
@@ -9,6 +9,7 @@ import { totalGot, totalInSpeedKb, totalOutSpeedKb, totalSent } from './throttle
9
import { getCurrentUsername } from './auth'
10
import { SendListReadable } from './SendList'
11
import { storedMap } from './persistence'
12
+import { countUniqueBy } from './cross'
13
14
export default {
15
@@ -29,25 +30,22 @@ export default {
30
get_connections({}, ctx) {
31
const list = new SendListReadable({
32
diff: true,
32
- addAtStart: getConnections().map(c =>
33
- !ignore(c) && serializeConnection(c)).filter(Boolean),
33
+ addAtStart: getConnections().map(serializeConnection),
34
})
35
type Change = Partial<Omit<Connection,'ip'>>
36
list.props({ you: ctx.ip })
37
return list.events(ctx, {
38
connection(conn: Connection) {
39
- if (ignore(conn)) return
39
list.add(serializeConnection(conn))
40
},
41
connectionClosed(conn: Connection) {
43
- if (ignore(conn)) return
42
list.remove(getConnAddress(conn))
43
},
44
connectionNewIp(conn: Connection, oldIp: string, newIp: string) {
45
list.update(getConnAddress(conn, oldIp), { ip: newIp })
46
},
47
connectionUpdated(conn: Connection, change: Change) {
50
- if (conn.socket.closed || ignore(conn) || ignore(change as any) || _.isEmpty(change)) return
48
+ if (conn.socket.closed || _.isEmpty(change)) return
49
if (change.ctx) {
50
Object.assign(change, fromCtx(change.ctx))
51
change.ctx = undefined
@@ -59,13 +57,13 @@ export default {
57
58
async *get_connection_stats() {
59
while (1) {
62
- const filtered = getConnections().filter(x => !ignore(x))
60
+ const connections = getConnections()
61
yield {
62
outSpeedKb: totalOutSpeedKb,
63
inSpeedKb: totalInSpeedKb,
64
sent_got: [totalSent.get(), totalGot.get(), totalGotSentResetTime.get()] as const,
67
- connections: filtered.length,
68
- ips: _.uniqBy(filtered, x => x.ip).length,
65
+ connections: connections.length,
66
+ ips: countUniqueBy(connections, conn => conn.ip),
67
}
68
await wait(1000)
69
}
@@ -80,10 +78,6 @@ export default {
78
79
} satisfies ApiHandlers
80
83
-function ignore(conn: Connection) {
84
- return false //conn.socket && isLocalHost(conn)
85
-}
86
-
81
export function serializeConnection(conn: Connection) {
82
const { socket, started, secure } = conn
83
return {
src/cross.ts
+16
@@ -198,6 +198,22 @@ export function onlyTruthy<T>(arr: T[]) {
198
return arr.filter(truthy)
199
}
200
201
+export function countUniqueBy<T, K>(items: Iterable<T>, keyFn: (item: T) => K, predicate?: (item: T) => boolean) {
202
+ // use a Set so unique counting stays linear even on very large live lists
203
+ const seen = new Set<K>()
204
+ let count = 0
205
+ for (const item of items) {
206
+ if (predicate && !predicate(item))
207
+ continue
208
+ const key = keyFn(item)
209
+ if (seen.has(key))
210
+ continue
211
+ seen.add(key)
212
+ count++
213
+ }
214
+ return count
215
+}
216
+
217
export function setHidden<T, ADD>(dest: T, src: ADD) {
218
return Object.defineProperties(dest, newObj(src as any, value => ({
219
enumerable: false,