Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { eq } from "drizzle-orm"
|
|
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"
|
|
|
|
export interface BoundIfaceTrafficDto {
|
|
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"
|
|
}
|
|
|
|
export interface UserTrafficDto {
|
|
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 {
|
|
rxNow: built.rxNow,
|
|
txNow: built.txNow,
|
|
rxPeak: built.rxPeak,
|
|
txPeak: built.txPeak,
|
|
rxTotal: built.rxTotalGiB,
|
|
txTotal: built.txTotalGiB,
|
|
rxSeries: built.rxSeries,
|
|
txSeries: built.txSeries,
|
|
}
|
|
}
|
|
|
|
async function serverStatus(serverId: number): Promise<"online" | "offline"> {
|
|
const row = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
|
|
if (!row?.enabled) return "offline"
|
|
return "online"
|
|
}
|
|
|
|
export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: number): Promise<UserTrafficDto[]> {
|
|
const sinceIso = new Date(rangeStartMs).toISOString()
|
|
const users = await listUsers()
|
|
const sampleCache = new Map<number, Awaited<ReturnType<typeof readServerSamplesInRange>>>()
|
|
|
|
return Promise.all(users.map(async (user) => {
|
|
const parts: BuiltTrafficSeries[] = []
|
|
const interfaces: BoundIfaceTrafficDto[] = []
|
|
|
|
for (const b of user.bindings) {
|
|
let rows = sampleCache.get(b.serverId)
|
|
if (!rows) {
|
|
rows = await readServerSamplesInRange(b.serverId, sinceIso)
|
|
sampleCache.set(b.serverId, rows)
|
|
}
|
|
const peerKey = b.peerPublicKey ?? ""
|
|
const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, b.interfaceName, peerKey)
|
|
parts.push(built)
|
|
const last = [...rows.filter((r) =>
|
|
r.interfaceName === b.interfaceName && (r.peerPublicKey ?? "") === peerKey,
|
|
)]
|
|
.sort((a, c) => a.sampledAt.localeCompare(c.sampledAt))
|
|
.at(-1)
|
|
const running = Boolean(last?.running) && !last?.disabled
|
|
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: running && (await serverStatus(b.serverId)) === "online" ? "online" : "offline",
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|