Files
MikrotikManager/backend/src/services/traffic-users.ts
T

146 lines
4.7 KiB
TypeScript

import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { listUsers } from "../modules/users/service/users-service.js"
import { readServerSamplesInRange } from "./traffic-collector.js"
import {
buildTrafficFromSamples,
mergeBuiltTraffic,
type BuiltTrafficSeries,
} from "./traffic-rate.js"
import { readTrafficCollectionContext } from "./traffic-collection-state.js"
import { applyTrafficFreshness, trafficBindingStatus } from "./traffic-freshness.js"
import type { TrafficObservations } from "./traffic-rate.js"
export interface BoundIfaceTrafficDto {
volumeUnit: "GiB"
observations?: TrafficObservations
id: string
bindingId: string
userId: string
userLogin: string
userName: string
interfaceName: string
interfaceType: string
peerPublicKey: string
peerName: string
comment: string
serverId: string
serverName: string
serverSite: string
serverCountry: string
rxNow: number
txNow: number
rxPeak: number
txPeak: number
rxTotal: number
txTotal: number
rxSeries: number[]
txSeries: number[]
status: "online" | "offline" | "unknown"
statusObservedAt: string | null
statusExpiresAt: string | null
}
export interface UserTrafficDto {
volumeUnit: "GiB"
observations?: TrafficObservations
id: string
login: string
displayName: string
role: string
active: boolean
interfaces: BoundIfaceTrafficDto[]
rxNow: number
txNow: number
rxPeak: number
txPeak: number
rxTotal: number
txTotal: number
rxSeries: number[]
txSeries: number[]
}
function seriesFromBuilt(built: BuiltTrafficSeries) {
return {
volumeUnit: "GiB" as const,
observations: built.observations,
rxNow: built.rxNow,
txNow: built.txNow,
rxPeak: built.rxPeak,
txPeak: built.txPeak,
rxTotal: built.rxTotalGiB,
txTotal: built.txTotalGiB,
rxSeries: built.rxSeries,
txSeries: built.txSeries,
}
}
export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: number): Promise<UserTrafficDto[]> {
const sinceIso = new Date(rangeStartMs).toISOString()
const users = await listUsers()
const [context, allServers] = await Promise.all([readTrafficCollectionContext(), db.select().from(servers)])
const catalog = new Map(allServers.map(s => [s.id, s]))
const sampleCache = new Map<number, ReturnType<typeof readServerSamplesInRange>>()
return Promise.all(users.map(async (user) => {
const parts: BuiltTrafficSeries[] = []
const interfaces: BoundIfaceTrafficDto[] = []
for (const b of user.bindings) {
let pending = sampleCache.get(b.serverId)
if (!pending) {
pending = readServerSamplesInRange(b.serverId, sinceIso)
sampleCache.set(b.serverId, pending)
}
const rows = await pending
const peerKey = b.peerPublicKey ?? ""
const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, b.interfaceName, peerKey, true)
const server = catalog.get(b.serverId)
if (built.observations) built.observations = applyTrafficFreshness(built.observations,
server ?? { enabled: false, updatedAt: "" }, context.settings, context.states.get(b.serverId), rangeEndMs)
parts.push(built)
const last = [...rows.filter((r) =>
r.interfaceName === b.interfaceName && (r.peerPublicKey ?? "") === peerKey,
)]
.sort((a, c) => Date.parse(a.sampledAt) - Date.parse(c.sampledAt))
.at(-1)
const status = trafficBindingStatus(last, !!peerKey, server ?? {enabled:false,updatedAt:""}, context.settings, context.states.get(b.serverId), rangeEndMs)
interfaces.push({
id: `${b.userId}:${b.serverId}:${b.interfaceName}:${peerKey || "_iface"}`,
bindingId: b.id,
userId: user.id,
userLogin: user.login,
userName: user.name,
interfaceName: b.interfaceName,
interfaceType: b.interfaceType,
peerPublicKey: peerKey,
peerName: b.peerName ?? "",
comment: b.comment,
serverId: String(b.serverId),
serverName: b.serverName,
serverSite: b.serverSite,
serverCountry: b.serverCountry,
...seriesFromBuilt(built),
status,
statusObservedAt: status !== "unknown" && last ? new Date(last.sampledAt).toISOString() : null,
statusExpiresAt: status !== "unknown" ? built.observations?.expiresAt ?? null : null,
})
}
const merged = mergeBuiltTraffic(parts)
return {
id: user.id,
login: user.login,
displayName: user.name,
role: user.role,
active: user.active,
interfaces,
...seriesFromBuilt(merged),
}
}))
}
export async function buildBoundInterfaceTraffic(rangeStartMs: number, rangeEndMs: number): Promise<BoundIfaceTrafficDto[]> {
return (await buildUserTrafficList(rangeStartMs, rangeEndMs)).flatMap((u) => u.interfaces)
}