refactor: replace custom API fetch logic with requestJson utility across multiple pages

Updated the API fetching mechanism in various components to utilize the new requestJson function for improved consistency and error handling. This change affects the alerts, dashboard, data collection, filters, gre, network map, probes, recursive routes, route optimizer, servers, settings, traffic, and uptime pages.
This commit is contained in:
Denozordec committed 2026-05-07 13:35:41 +07:00
1 parent 5f31bb47fb
commit 6d8379501c
42 files changed
+1336 -631

No files matched your search

+8 -88
View File
@@ -1,5 +1,4 @@
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { z } from "zod"
import {
getAlertsMeta,
getTelegramBotToken,
@@ -13,90 +12,11 @@ import {
sendTelegramAlertMessage,
updateTelegramSettings,
} from "../services/alerts-service.js"
const AlertTypeSchema = z.enum([
"gre-tunnel",
"bgp-peer",
"bgp-prefix",
"gre-client",
"server",
"rtt",
"loss",
"traffic",
])
const AlertSeveritySchema = z.enum(["critical", "warning", "info"])
const AlertCooldownSchema = z.enum(["1м", "5м", "15м", "1ч", "4ч", "24ч"])
const RecoveryModeSchema = z.enum(["always", "never", "conditional"])
const AlertGroupBodySchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
combineMode: z.enum(["any", "all"]),
enabled: z.boolean(),
cooldownOverride: z.union([AlertCooldownSchema, z.null()]),
})
const AlertRuleBodySchema = z
.object({
id: z.string().min(1),
name: z.string().min(1),
type: AlertTypeSchema,
target: z.string().optional(),
targets: z.array(z.string().min(1)).optional(),
groupId: z.string().nullable().optional(),
/** Сводка; если переданы `conditions`, может дублировать первую строку */
condition: z.string().optional(),
/** Несколько условий (OR); приоритет над одной строкой `condition` */
conditions: z.array(z.string().min(1)).optional(),
severity: AlertSeveritySchema,
enabled: z.boolean(),
cooldown: AlertCooldownSchema,
/** Секунды подтверждения стабильности; 0 / null / не передано — выкл. */
confirmStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
recoveryMode: RecoveryModeSchema.optional(),
recoveryStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
chatId: z.string(),
})
.refine((r) => (r.targets != null && r.targets.length > 0) || Boolean(r.target?.trim()), {
message: "Нужен хотя бы один объект: targets или target",
path: ["targets"],
})
.refine(
(r) =>
(r.conditions != null && r.conditions.length > 0) || Boolean(r.condition?.trim()),
{ message: "Нужно хотя бы одно условие: conditions или condition", path: ["conditions"] },
)
const PutRulesSchema = z.object({
rules: z.array(AlertRuleBodySchema),
groups: z.array(AlertGroupBodySchema).optional(),
})
const PutTelegramSchema = z.object({
/** undefined — не менять; null или "" — очистить токен */
token: z.union([z.string(), z.null()]).optional(),
chatId: z.string().optional(),
/** undefined — не менять; null — сбросить (общий чат супергруппы) */
messageThreadId: z.union([z.number().int().positive(), z.null()]).optional(),
})
const RulePreviewTelegramSchema = z.object({
name: z.string().min(1),
targets: z.array(z.string().min(1)).min(1),
conditionLine: z.string().min(1),
severity: AlertSeveritySchema,
cooldown: AlertCooldownSchema,
})
const TestTelegramSchema = z.object({
/** Если не передан — взять сохранённый токен из БД */
token: z.string().optional(),
chatId: z.string().optional(),
/** Если не передан — из БД; для проверки черновика до сохранения */
messageThreadId: z.coerce.number().int().positive().optional(),
/** Если задан — вместо короткого текста отправляется предпросмотр правила (создание / правка). */
rulePreview: RulePreviewTelegramSchema.optional(),
})
import {
putRulesSchema,
putTelegramSchema,
testTelegramSchema,
} from "../../../packages/contracts/dist/alerts.js"
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/alerts", async (_req, reply) => {
@@ -112,7 +32,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.put("/alerts/rules", async (req, reply) => {
const parsed = PutRulesSchema.safeParse(req.body)
const parsed = putRulesSchema.safeParse(req.body)
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
@@ -152,7 +72,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.put("/alerts/telegram", async (req, reply) => {
const parsed = PutTelegramSchema.safeParse(req.body)
const parsed = putTelegramSchema.safeParse(req.body)
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
@@ -166,7 +86,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.post("/alerts/telegram/test", async (req, reply) => {
const parsed = TestTelegramSchema.safeParse(req.body ?? {})
const parsed = testTelegramSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
+5 -3
View File
@@ -4,14 +4,15 @@ import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { parseBgpSessions } from "../services/bgp-parse-sessions.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { ServerIdParamSchema } from "../types/server.js"
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
import type { BgpSessionRead } from "../types/server.js"
const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
app.get("/bgp/sessions/raw/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const server = db.select().from(servers).where(eq(servers.id, req.params.id)).limit(1).all()[0]
const params = req.params as ServerIdParams
const server = db.select().from(servers).where(eq(servers.id, params.id)).limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
const client = MikrotikClient.fromServer(server)
@@ -44,9 +45,10 @@ const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/servers/:id/bgp/sessions — single server
app.get("/servers/:id/bgp/sessions", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.where(eq(servers.id, params.id))
.limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+3 -2
View File
@@ -4,7 +4,7 @@ import { z } from "zod"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { ServerIdParamSchema } from "../types/server.js"
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
import type {
RosIpAddress, RosInterface, RosResource, RosIdentity, RosIpRoute,
RosFirewallFilter, RosLogEntry, RosBgpSession, RosPingResult,
@@ -313,9 +313,10 @@ const execRoutes: FastifyPluginAsyncZod = async (app) => {
"/servers/:id/exec",
{ schema: { params: ServerIdParamSchema, body: ExecBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.where(eq(servers.id, params.id))
.limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+7 -4
View File
@@ -3,7 +3,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { serverSnapshots, servers, trafficSamples, uptimeSpeedProbes } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { ServerIdParamSchema } from "../types/server.js"
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
import type {
RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
RosBfdSession,
@@ -630,9 +630,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/servers/:id/ospf — single server OSPF + BFD data
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.where(eq(servers.id, params.id))
.limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
@@ -657,9 +658,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
"/servers/:id/ospf/optimize/preview",
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.where(eq(servers.id, params.id))
.limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
@@ -688,9 +690,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
"/servers/:id/ospf/optimize",
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.where(eq(servers.id, params.id))
.limit(1).all()[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+36 -161
View File
@@ -1,95 +1,28 @@
import { desc, eq } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { db } from "../db/index.js"
import { servers, serverSnapshots } from "../db/schema.js"
import type { ServerRead, WanUplinkRead } from "../types/server.js"
import {
ServerCreateSchema,
ServerUpdateSchema,
ServerIdParamSchema,
SnapshotsQuerySchema,
TestConnectionSchema,
type ServerCreateRequest,
type ServerIdParams,
type ServerUpdateRequest,
type SnapshotsQuery,
type TestConnectionRequest,
} from "../types/server.js"
import { pollServer, toSnapshotRead } from "../services/poller.js"
import { pollServer } from "../services/poller.js"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
// ── helpers ────────────────────────────────────────────────────────────────────
type ServerRow = typeof servers.$inferSelect
type SnapshotRow = typeof serverSnapshots.$inferSelect
function parseWanUplinksJson(raw: string | null | undefined): WanUplinkRead[] {
if (raw == null || raw === "") return []
try {
const data = JSON.parse(raw) as unknown
if (!Array.isArray(data)) return []
const out: WanUplinkRead[] = []
for (const row of data) {
if (typeof row !== "object" || row === null) continue
const r = row as Record<string, unknown>
out.push({
id: typeof r.id === "string" ? r.id : "",
name: typeof r.name === "string" ? r.name : "",
isp: typeof r.isp === "string" ? r.isp : "",
iface: typeof r.iface === "string" ? r.iface : "",
ip: typeof r.ip === "string" ? r.ip : "",
maxDl: typeof r.maxDl === "number" && Number.isFinite(r.maxDl) ? r.maxDl : 0,
maxUl: typeof r.maxUl === "number" && Number.isFinite(r.maxUl) ? r.maxUl : 0,
})
}
return out
} catch {
return []
}
}
/** Merge a server row with its latest snapshot into the frontend-compatible shape */
function toServerRead(server: ServerRow, snap: SnapshotRow | undefined): ServerRead {
return {
id: server.id,
name: server.name || server.host,
host: server.host,
port: server.port,
useSsl: server.useSsl,
verifySsl: server.verifySsl,
username: server.username,
password: server.password,
type: server.type,
site: server.site,
country: server.country,
asn: server.asn,
comment: server.comment,
enabled: server.enabled,
lanSubnet: server.lanSubnet ?? "",
wanUplinks: parseWanUplinksJson(server.wanUplinks),
createdAt: server.createdAt,
updatedAt: server.updatedAt,
// snapshot fields (null if never polled)
status: snap ? snap.status : null,
latency: snap?.latencyMs ?? null,
os: snap?.rosVersion ?? null,
model: snap?.boardName ?? null,
uptime: snap?.uptime ?? null,
cpuLoad: snap?.cpuLoad ?? null,
freeMemory: snap?.freeMemory ?? null,
totalMemory: snap?.totalMemory ?? null,
identityName: snap?.identityName ?? null,
sessions: 0,
polledAt: snap?.polledAt ?? null,
}
}
/** Get latest snapshot for a single server */
function getLatestSnapshot(serverId: number): SnapshotRow | undefined {
return db
.select()
.from(serverSnapshots)
.where(eq(serverSnapshots.serverId, serverId))
.orderBy(desc(serverSnapshots.polledAt))
.limit(1)
.all()[0]
}
import {
createServer,
deleteServer,
getServerReadById,
listServerSnapshots,
listServersRead,
updateServer,
} from "../modules/servers/service/servers-service.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
// ── plugin ─────────────────────────────────────────────────────────────────────
@@ -97,7 +30,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// POST /api/servers/test-connection — check credentials before saving
app.post("/test-connection", { schema: { body: TestConnectionSchema } }, async (req, reply) => {
const { host, port, useSsl, verifySsl, apiPath, username, password } = req.body
const { host, port, useSsl, verifySsl, apiPath, username, password } = req.body as TestConnectionRequest
const client = new MikrotikClient({ host, port, useSsl, verifySsl, apiPath, username, password })
try {
@@ -144,17 +77,13 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/servers
app.get("/", async (_req, reply) => {
const all = db.select().from(servers).all()
const result: ServerRead[] = all.map(s => toServerRead(s, getLatestSnapshot(s.id)))
return reply.send(result)
return reply.send(listServersRead())
})
// GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
app.get("/:id/ros-src-address", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const server = getServerRowById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
const ipv4 = await resolveRosSrcIpv4(server.host)
@@ -163,30 +92,15 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// POST /api/servers
app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
const now = new Date().toISOString()
const { wanUplinks, ...rest } = req.body
const [inserted] = db
.insert(servers)
.values({
...rest,
wanUplinks: JSON.stringify(wanUplinks ?? []),
createdAt: now,
updatedAt: now,
})
.returning()
.all()
return reply.status(201).send(toServerRead(inserted, undefined))
return reply.status(201).send(createServer(req.body as ServerCreateRequest))
})
// GET /api/servers/:id
app.get("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const server = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const server = getServerReadById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
return reply.send(toServerRead(server, getLatestSnapshot(server.id)))
return reply.send(server)
})
// PUT /api/servers/:id
@@ -194,58 +108,30 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
"/:id",
{ schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
async (req, reply) => {
const existing = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
const body = req.body
const { wanUplinks, ...rest } = body
const setPayload: Record<string, unknown> = { updatedAt: new Date().toISOString() }
for (const [k, v] of Object.entries(rest)) {
if (v !== undefined) setPayload[k] = v
}
if (wanUplinks !== undefined) {
setPayload.wanUplinks = JSON.stringify(wanUplinks)
}
const [updated] = db
.update(servers)
.set(setPayload as Partial<ServerRow>)
.where(eq(servers.id, req.params.id))
.returning()
.all()
return reply.send(toServerRead(updated, getLatestSnapshot(updated.id)))
return reply.send(updateServer(params.id, req.body as ServerUpdateRequest))
},
)
// DELETE /api/servers/:id
app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const existing = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
db.delete(servers).where(eq(servers.id, req.params.id)).run()
deleteServer(params.id)
return reply.status(204).send()
})
// POST /api/servers/:id/poll
app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const existing = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
try {
const snap = await pollServer(req.params.id)
const snap = await pollServer(params.id)
return reply.send(snap)
} catch (err) {
return reply.status(500).send({ error: (err as Error).message })
@@ -257,22 +143,11 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
"/:id/snapshots",
{ schema: { params: ServerIdParamSchema, querystring: SnapshotsQuerySchema } },
async (req, reply) => {
const existing = db
.select().from(servers)
.where(eq(servers.id, req.params.id))
.limit(1).all()[0]
const params = req.params as ServerIdParams
const query = req.query as SnapshotsQuery
const existing = getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
const snaps = db
.select()
.from(serverSnapshots)
.where(eq(serverSnapshots.serverId, req.params.id))
.orderBy(desc(serverSnapshots.polledAt))
.limit(req.query.limit)
.all()
return reply.send(snaps.map(toSnapshotRead))
return reply.send(listServerSnapshots(params.id, query.limit))
},
)
}