feat(db): перевести хранилище с SQLite на PostgreSQL
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 <cursoragent@cursor.com>
This commit is contained in:
DenozordecandCursor committed 2026-09-08 01:36:48 +07:00
1 parent 0e5fb065e2
commit ec43591a99
128 files changed
+4304 -3639

No files matched your search

+12 -12
View File
@@ -20,14 +20,14 @@ import {
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/alerts", async (_req, reply) => {
const tg = getTelegramPublic()
const tg = await getTelegramPublic()
const connected = tg.tokenConfigured && Boolean(tg.chatId?.trim())
return reply.send({
telegram: { ...tg, connected },
groups: listAlertGroups(),
rules: listAlertRules(),
history: listMergedHistory(),
meta: getAlertsMeta(),
groups: await listAlertGroups(),
rules: await listAlertRules(),
history: await listMergedHistory(),
meta: await getAlertsMeta(),
})
})
@@ -38,7 +38,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
}
const groups =
parsed.data.groups ??
listAlertGroups().map((g) => ({
(await listAlertGroups()).map((g) => ({
id: g.id,
name: g.name,
combineMode: g.combineMode,
@@ -67,8 +67,8 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
recoveryStabilitySec: r.recoveryStabilitySec ?? null,
chatId: r.chatId,
}))
replaceAlertsConfig({ groups, rules })
return reply.send({ ok: true, groups: listAlertGroups(), rules: listAlertRules() })
await replaceAlertsConfig({ groups, rules })
return reply.send({ ok: true, groups: await listAlertGroups(), rules: await listAlertRules() })
})
app.put("/alerts/telegram", async (req, reply) => {
@@ -76,7 +76,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const next = updateTelegramSettings({
const next = await updateTelegramSettings({
token: parsed.data.token,
chatId: parsed.data.chatId,
messageThreadId: parsed.data.messageThreadId,
@@ -90,9 +90,9 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const fromDb = getTelegramBotToken()
const fromDb = await getTelegramBotToken()
const token = (parsed.data.token?.trim() || fromDb).trim()
const pub = getTelegramPublic()
const pub = await getTelegramPublic()
const chatId = (parsed.data.chatId?.trim() || pub.chatId || "").trim()
if (!token) {
return reply.status(400).send({ error: "Не задан Bot Token (сохраните в БД или передайте в запросе)" })
@@ -103,7 +103,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
const threadId =
parsed.data.messageThreadId != null && parsed.data.messageThreadId >= 1
? parsed.data.messageThreadId
: getTelegramMessageThreadIdForApi()
: await getTelegramMessageThreadIdForApi()
const text = parsed.data.rulePreview
? formatAlertRuleTestTelegramText(parsed.data.rulePreview)
: "MikroTik Manager — тест оповещений (Telegram)."
+11 -11
View File
@@ -61,7 +61,7 @@ async function processBackupJob(job: BackupJob, ids: string[], notes?: string) {
}
job.status = "done"
job.finishedAt = new Date().toISOString()
appendEvent({
await appendEvent({
level: job.failures.length > 0 ? "warning" : "info",
eventType: "backups.job.done",
sourceModule: "backups",
@@ -80,11 +80,11 @@ async function processBackupJob(job: BackupJob, ids: string[], notes?: string) {
const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/backups", async (_req, reply) => {
return reply.send(listBackups())
return reply.send(await listBackups())
})
app.get("/backups/schedule", async (_req, reply) => {
return reply.send(getBackupScheduleSettings())
return reply.send(await getBackupScheduleSettings())
})
app.put("/backups/schedule", async (req, reply) => {
@@ -92,15 +92,15 @@ const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const result = updateBackupScheduleSettings(parsed.data)
refreshScheduler()
const result = await updateBackupScheduleSettings(parsed.data)
await refreshScheduler()
return reply.send(result)
})
app.post("/backups/create", { schema: { body: CreateBackupBodySchema } }, async (req, reply) => {
const inputIds = req.body.serverIds.map((x) => String(x))
const notes = req.body.notes?.trim() || undefined
const existingServers = new Set(listServersRead().map((s) => String(s.id)))
const existingServers = new Set((await listServersRead()).map((s) => String(s.id)))
const ids = [...new Set(inputIds)].filter((id) => existingServers.has(id))
if (ids.length === 0) return reply.status(400).send({ error: "Не выбраны валидные серверы" })
@@ -115,7 +115,7 @@ const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
failures: [],
}
backupJobs.set(jobId, job)
appendEvent({
await appendEvent({
level: "info",
eventType: "backups.job.started",
sourceModule: "backups",
@@ -128,8 +128,8 @@ const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
notes: notes ?? null,
},
})
queueMicrotask(() => {
void processBackupJob(job, ids, notes).catch((err) => {
queueMicrotask(async () => {
void processBackupJob(job, ids, notes).catch(async (err) => {
job.status = "failed"
job.finishedAt = new Date().toISOString()
const failure = {
@@ -137,7 +137,7 @@ const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
error: err instanceof Error ? err.message : String(err),
}
job.failures.push(failure)
appendEvent({
await appendEvent({
level: "critical",
eventType: "backups.job.failed",
sourceModule: "backups",
@@ -167,7 +167,7 @@ const backupsRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/backups/:id/download", { schema: { params: BackupIdParamSchema } }, async (req, reply) => {
const hit = getBackupById(req.params.id)
const hit = await getBackupById(req.params.id)
if (!hit) return reply.status(404).send({ error: "Бэкап не найден" })
const filePath = path.join(getBackupsDir(), hit.filename)
const content = await readFile(filePath, "utf8").catch(() => null)
+4 -4
View File
@@ -12,7 +12,7 @@ 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 params = req.params as ServerIdParams
const server = db.select().from(servers).where(eq(servers.id, params.id)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
const client = MikrotikClient.fromServer(server)
@@ -26,7 +26,7 @@ const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/bgp/sessions — aggregate from ALL enabled servers
app.get("/bgp/sessions", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const results: BgpSessionRead[][] = await Promise.all(
allServers.map(async (server) => {
@@ -46,10 +46,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
const server = (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+10 -10
View File
@@ -32,7 +32,7 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/certificates/acme-settings", async (_req, reply) => {
return reply.send(getAcmeSettingsPublic())
return reply.send(await getAcmeSettingsPublic())
})
app.put("/certificates/acme-settings", async (req, reply) => {
@@ -40,11 +40,11 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
return reply.send(updateAcmeSettings(parsed.data))
return reply.send(await updateAcmeSettings(parsed.data))
})
app.get("/certificates/renew-settings", async (_req, reply) => {
return reply.send(getCertificateRenewSettings())
return reply.send(await getCertificateRenewSettings())
})
app.put("/certificates/renew-settings", async (req, reply) => {
@@ -52,8 +52,8 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const result = updateCertificateRenewSettings(parsed.data)
refreshScheduler()
const result = await updateCertificateRenewSettings(parsed.data)
await refreshScheduler()
return reply.send(result)
})
@@ -62,7 +62,7 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const effective = parsed.data.cloudflareApiToken?.trim() || getAcmeCloudflareToken()
const effective = parsed.data.cloudflareApiToken?.trim() || await getAcmeCloudflareToken()
if (!effective) {
return reply.status(400).send({ error: "Не задан Cloudflare API token" })
}
@@ -83,12 +83,12 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const serverId = String(parsed.data.serverId)
const server = getServerRowByIdString(serverId)
const server = await getServerRowByIdString(serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const jobId = randomUUID()
const trustStore = (parsed.data.trustStore ?? ["www", "api"]).join(",")
createIssueJobRecord({
await createIssueJobRecord({
id: jobId,
serverId,
certName: parsed.data.certName.trim(),
@@ -103,9 +103,9 @@ const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/certificates/issue/:jobId", async (req, reply) => {
const jobId = String((req.params as { jobId: string }).jobId)
const row = getIssueJobRecord(jobId)
const row = await getIssueJobRecord(jobId)
if (!row) return reply.status(404).send({ error: "Задача не найдена" })
return reply.send(toIssueJobDto(row))
return reply.send(await toIssueJobDto(row))
})
}
+2 -2
View File
@@ -8,7 +8,7 @@ const eventsRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректные параметры запроса", details: parsed.error.flatten() })
}
return reply.send({ events: readEvents(parsed.data) })
return reply.send({ events: await readEvents(parsed.data) })
})
app.post("/events/batch", async (req, reply) => {
@@ -16,7 +16,7 @@ const eventsRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
appendEvents(parsed.data.events)
await appendEvents(parsed.data.events)
return reply.status(201).send({ ok: true, inserted: parsed.data.events.length })
})
}
+17 -19
View File
@@ -17,11 +17,11 @@ const TestEvobgpSchema = z.object({
apiKey: z.string().optional(),
})
function ensureEvobgpRow() {
let row = db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1).all()[0]
async function ensureEvobgpRow() {
let row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0]
if (!row) {
db.insert(evobgpSettings).values({ id: 1 }).run()
row = db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1).all()[0]
await db.insert(evobgpSettings).values({ id: 1 })
row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0]
}
return row!
}
@@ -108,11 +108,10 @@ function aggregateCommunityRouteCounts(catalog: EvoCatalogRaw): Map<string, numb
* Уникальные серверы из локальной БД, у которых в BGP-фильтре указан community (строка AS:NNN).
* Ключ — значение `filter_rules.community` после trim (как в EvoBGP `community`).
*/
function distinctServersByFilterCommunityValue(): Map<string, number> {
const rows = db
async function distinctServersByFilterCommunityValue(): Promise<Map<string, number>> {
const rows = await db
.select({ serverId: filterRules.serverId, community: filterRules.community })
.from(filterRules)
.all()
const byVal = new Map<string, Set<number>>()
for (const r of rows) {
const v = (r.community ?? "").trim()
@@ -161,8 +160,8 @@ async function fetchEvoJson<T>(root: string, path: string, token: string): Promi
}
}
function credentialsFromDb(): { root: string; apiKey: string } | null {
const row = ensureEvobgpRow()
async function credentialsFromDb(): Promise<{ root: string; apiKey: string } | null> {
const row = await ensureEvobgpRow()
const root = normalizeBaseUrl(row.baseUrl)
const apiKey = normalizeApiKey(row.apiKey)
if (!root || !apiKey) return null
@@ -171,7 +170,7 @@ function credentialsFromDb(): { root: string; apiKey: string } | null {
const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/evobgp/settings", async (_req, reply) => {
const row = ensureEvobgpRow()
const row = await ensureEvobgpRow()
return reply.send({
baseUrl: row.baseUrl ?? "",
enabled: Boolean(row.enabled),
@@ -184,7 +183,7 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const cur = ensureEvobgpRow()
const cur = await ensureEvobgpRow()
let nextBase = cur.baseUrl
let nextEnabled = cur.enabled
let nextKey = cur.apiKey
@@ -200,17 +199,16 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
: normalizeApiKey(parsed.data.apiKey)
}
db.update(evobgpSettings)
await db.update(evobgpSettings)
.set({
baseUrl: nextBase,
enabled: nextEnabled,
apiKey: nextKey,
updatedAt: sql`(datetime('now'))`,
updatedAt: sql`now()`,
})
.where(eq(evobgpSettings.id, 1))
.run()
const row = ensureEvobgpRow()
const row = await ensureEvobgpRow()
return reply.send({
baseUrl: row.baseUrl ?? "",
enabled: Boolean(row.enabled),
@@ -228,7 +226,7 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const row = ensureEvobgpRow()
const row = await ensureEvobgpRow()
const d = parsed.data
const urlRaw = d.baseUrl !== undefined ? d.baseUrl : row.baseUrl
const keyRaw =
@@ -253,11 +251,11 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
* Прокси к GET /v1/router-lists/catalog — учётные данные только из БД.
*/
app.post("/evobgp/catalog", async (_req, reply) => {
const row = ensureEvobgpRow()
const row = await ensureEvobgpRow()
if (!row.enabled) {
return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" })
}
const cred = credentialsFromDb()
const cred = await credentialsFromDb()
if (!cred) {
return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" })
}
@@ -333,7 +331,7 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
})
const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog)
const serversByCommunityStr = distinctServersByFilterCommunityValue()
const serversByCommunityStr = await distinctServersByFilterCommunityValue()
const communities = commItems.map((c) => {
const valueStr = (c.community ?? "").trim()
+2 -2
View File
@@ -314,10 +314,10 @@ const execRoutes: FastifyPluginAsyncZod = async (app) => {
{ schema: { params: ServerIdParamSchema, body: ExecBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
const server = (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+35 -39
View File
@@ -186,12 +186,11 @@ function gatewayFromRecursiveDst(dstAddress: string): string {
* Импорт с роутера даёт только `set gateway` без `set out-interface` — в таком виде не отличить от «поломанного» GRE.
* Сопоставляем hop с локальной таблицей recursive_routes и восстанавливаем gatewayTunnelId = rec:<id>.
*/
function enrichRulesWithRecursiveGateway(serverId: number, rules: ApiFilterRule[]): ApiFilterRule[] {
const rows = db
async function enrichRulesWithRecursiveGateway(serverId: number, rules: ApiFilterRule[]): Promise<ApiFilterRule[]> {
const rows = await db
.select()
.from(recursiveRoutes)
.where(and(eq(recursiveRoutes.serverId, serverId), eq(recursiveRoutes.disabled, false)))
.all()
return rules.map(rule => {
if (rule.action === "blackhole") return rule
@@ -249,7 +248,7 @@ async function fetchServerFilters(server: ServerRow) {
.filter(r => (r.chain ?? "").trim().toLowerCase() === "bgp-in")
.flatMap(parseFilterRule)
const rules = enrichRulesWithRecursiveGateway(server.id, rulesRaw)
const rules = await enrichRulesWithRecursiveGateway(server.id, rulesRaw)
return {
serverId: String(server.id),
@@ -258,8 +257,8 @@ async function fetchServerFilters(server: ServerRow) {
}
}
function toApiRulesets(serverRows: ServerRow[]) {
const dbRules = db.select().from(filterRules).orderBy(asc(filterRules.serverId), asc(filterRules.sortOrder)).all()
async function toApiRulesets(serverRows: ServerRow[]) {
const dbRules = await db.select().from(filterRules).orderBy(asc(filterRules.serverId), asc(filterRules.sortOrder))
return serverRows.map(s => ({
serverId: String(s.id),
rules: dbRules
@@ -277,7 +276,7 @@ function toApiRulesets(serverRows: ServerRow[]) {
}
/** GRE: gatewayTunnelId = имя интерфейса; рекурсивный: rec:<id строки recursive_routes */
function resolveRouteTargets(serverId: number, rule: ApiFilterRule): { gateway: string; outIface: string } {
async function resolveRouteTargets(serverId: number, rule: ApiFilterRule): Promise<{ gateway: string; outIface: string }> {
if (rule.action === "blackhole") return { gateway: "", outIface: "" }
const tid = (rule.gatewayTunnelId ?? "").trim()
if (tid.startsWith("rec:")) {
@@ -285,12 +284,11 @@ function resolveRouteTargets(serverId: number, rule: ApiFilterRule): { gateway:
if (!Number.isFinite(rid)) {
return { gateway: rule.gateway, outIface: "" }
}
const row = db
const row = (await db
.select()
.from(recursiveRoutes)
.where(and(eq(recursiveRoutes.serverId, serverId), eq(recursiveRoutes.id, rid)))
.limit(1)
.all()[0]
.limit(1))[0]
if (!row) return { gateway: rule.gateway, outIface: "" }
const gw = gatewayFromRecursiveDst(row.dstAddress)
return { gateway: gw || rule.gateway, outIface: "" }
@@ -303,30 +301,30 @@ function normalizeCommunity(c: string): string {
}
/** Одинаковый эффект на роутере при одинаковой community (blackhole vs gateway + out-interface) */
function ruleEffectSignature(serverId: number, r: ApiFilterRule): string {
async function ruleEffectSignature(serverId: number, r: ApiFilterRule): Promise<string> {
if (r.action === "blackhole") return `bh:${normalizeCommunity(r.community)}`
const { gateway, outIface } = resolveRouteTargets(serverId, r)
const { gateway, outIface } = await resolveRouteTargets(serverId, r)
return `rt:${normalizeCommunity(r.community)}:${gateway}:${outIface}`
}
export type FilterRouterCompareStatus = "synced" | "drift" | "missing"
function compareDbRulesWithRouter(
async function compareDbRulesWithRouter(
serverId: number,
dbRules: ApiFilterRule[],
remoteRules: ApiFilterRule[],
): Record<string, FilterRouterCompareStatus> {
): Promise<Record<string, FilterRouterCompareStatus>> {
const remoteSigByComm = new Map<string, string>()
for (const rr of remoteRules) {
const c = normalizeCommunity(rr.community)
if (!remoteSigByComm.has(c)) {
remoteSigByComm.set(c, ruleEffectSignature(serverId, rr))
remoteSigByComm.set(c, await ruleEffectSignature(serverId, rr))
}
}
const out: Record<string, FilterRouterCompareStatus> = {}
for (const dr of dbRules) {
const c = normalizeCommunity(dr.community)
const sigD = ruleEffectSignature(serverId, dr)
const sigD = await ruleEffectSignature(serverId, dr)
const sigR = remoteSigByComm.get(c)
if (sigR === undefined) {
out[c] = "missing"
@@ -339,7 +337,7 @@ function compareDbRulesWithRouter(
return out
}
function toRouterRuleBody(serverId: number, rules: ApiFilterRule[]): string {
async function toRouterRuleBody(serverId: number, rules: ApiFilterRule[]): Promise<string> {
if (rules.length === 0) return ""
// Группируем по эффекту (action + gateway + out-interface). Communities с одним и тем же
// `set gw` объединяются через `||` в один if-блок — компактнее и ближе к привычному
@@ -361,7 +359,7 @@ function toRouterRuleBody(serverId: number, rules: ApiFilterRule[]): string {
const isBlackhole = rule.action === "blackhole"
const { gateway, outIface } = isBlackhole
? { gateway: "", outIface: "" }
: resolveRouteTargets(serverId, rule)
: await resolveRouteTargets(serverId, rule)
const key = isBlackhole ? "bh" : `rt:${gateway}:${outIface}`
let idx = indexByKey.get(key)
if (idx === undefined) {
@@ -404,10 +402,10 @@ function toRouterRuleBody(serverId: number, rules: ApiFilterRule[]): string {
}
async function replaceDbRules(serverId: number, rules: ApiFilterRule[]) {
db.delete(filterRules).where(eq(filterRules.serverId, serverId)).run()
await db.delete(filterRules).where(eq(filterRules.serverId, serverId))
if (rules.length === 0) return
const now = new Date().toISOString()
db.insert(filterRules).values(
await db.insert(filterRules).values(
rules.map((r, i) => ({
serverId,
sortOrder: i,
@@ -420,7 +418,7 @@ async function replaceDbRules(serverId: number, rules: ApiFilterRule[]) {
createdAt: now,
updatedAt: now,
})),
).run()
)
}
const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
@@ -432,17 +430,16 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "serverId is required" })
}
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
const remote = await fetchServerFilters(server)
const rows = db
const rows = await db
.select()
.from(filterRules)
.where(eq(filterRules.serverId, serverId))
.orderBy(asc(filterRules.sortOrder))
.all()
const dbRules: ApiFilterRule[] = rows.map(r => ({
id: String(r.id),
@@ -454,7 +451,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
description: r.description,
}))
const byCommunity = compareDbRulesWithRouter(serverId, dbRules, remote.rules)
const byCommunity = await compareDbRulesWithRouter(serverId, dbRules, remote.rules)
return reply.send({ byCommunity })
} catch (err) {
app.log.error({ serverId, err: String(err) }, "filters router-compare failed")
@@ -466,7 +463,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/filters/gre-tunnels", async (req, reply) => {
const q = req.query as { serverId?: string | number }
const sid = parseDbServerId(q.serverId)
const enabledServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
if (sid !== null) {
const server = enabledServers.find(s => s.id === sid)
@@ -492,8 +489,8 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
/** Только правила фильтров из БД (без опроса MikroTik за GRE) */
app.get("/filters/rules", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const dbRulesets = toApiRulesets(allServers)
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const dbRulesets = await toApiRulesets(allServers)
return reply.send({
rulesets: dbRulesets,
@@ -506,7 +503,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
const payload = body.rulesets ?? []
const serverIds = payload.map(r => Number.parseInt(r.serverId, 10)).filter(Number.isFinite)
if (serverIds.length > 0) {
db.delete(filterRules).where(inArray(filterRules.serverId, serverIds)).run()
await db.delete(filterRules).where(inArray(filterRules.serverId, serverIds))
}
for (const rs of payload) {
const sid = Number.parseInt(rs.serverId, 10)
@@ -524,12 +521,12 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "serverId is required" })
}
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
app.log.info({ serverId, host: server.host }, "Filters sync from router started")
appendEvent({
await appendEvent({
level: "info",
eventType: "filters.sync.from_router.started",
sourceModule: "filters",
@@ -541,7 +538,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
const remote = await fetchServerFilters(server)
await replaceDbRules(server.id, remote.rules)
app.log.info({ serverId, totalRules: remote.rules.length }, "Filters sync from router completed")
appendEvent({
await appendEvent({
level: "info",
eventType: "filters.sync.from_router.done",
sourceModule: "filters",
@@ -553,7 +550,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.send({ ok: true, updatedServers: 1, totalRules: remote.rules.length, serverId })
} catch (err) {
app.log.error({ serverId, err: String(err) }, "Filters sync from router failed")
appendEvent({
await appendEvent({
level: "critical",
eventType: "filters.sync.from_router.failed",
sourceModule: "filters",
@@ -570,7 +567,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
const body = req.body as { serverId?: string | number } | undefined
const requestedServerId = parseDbServerId(body?.serverId)
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const targetServers = requestedServerId !== null
? allServers.filter(s => s.id === requestedServerId)
: allServers
@@ -582,7 +579,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
let updatedServers = 0
let pushedRules = 0
const errors: Array<{ serverId: number; error: string }> = []
appendEvent({
await appendEvent({
level: "info",
eventType: "filters.sync.to_router.started",
sourceModule: "filters",
@@ -618,10 +615,9 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
.map(r => r[".id"])
.filter((id): id is string => Boolean(id))
const rows = db.select().from(filterRules)
const rows = await db.select().from(filterRules)
.where(and(eq(filterRules.serverId, server.id)))
.orderBy(asc(filterRules.sortOrder))
.all()
const rules: ApiFilterRule[] = rows.map(r => ({
id: String(r.id),
@@ -639,7 +635,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
// трактует как «вызов команды» и отдаёт 400 «no such command».
// См. https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
if (rules.length > 0) {
const ruleBody = toRouterRuleBody(server.id, rules)
const ruleBody = await toRouterRuleBody(server.id, rules)
if (managedRule && managedRule[".id"]) {
await client.patch(
`/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`,
@@ -688,7 +684,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
}
}
appendEvent({
await appendEvent({
level: errors.length === 0 ? "info" : "warning",
eventType: errors.length === 0 ? "filters.sync.to_router.done" : "filters.sync.to_router.partial",
sourceModule: "filters",
+11 -11
View File
@@ -116,8 +116,8 @@ function rosErr(e: unknown): string {
return String(e)
}
function requireServer(serverId: string) {
return getEnabledServerById(serverId)
async function requireServer(serverId: string) {
return await getEnabledServerById(serverId)
}
const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
@@ -132,7 +132,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = firewallRestPath(body.family as FirewallFamily, body.table as FirewallTable)
@@ -150,7 +150,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family as FirewallFamily, body.table as FirewallTable)}/${encodeRosId(body.rosId)}`
@@ -168,7 +168,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, body.table)}/${encodeRosId(body.rosId)}`
@@ -186,7 +186,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, body.table)}/${encodeRosId(body.rosId)}`
@@ -204,7 +204,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, body.table)}/move`
@@ -225,7 +225,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
try {
@@ -242,7 +242,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, "address-list")}/${encodeRosId(body.rosId)}`
@@ -260,7 +260,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, "address-list")}/${encodeRosId(body.rosId)}`
@@ -278,7 +278,7 @@ const firewallRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = requireServer(body.serverId)
const server = await requireServer(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const path = `${firewallRestPath(body.family, "address-list")}/${encodeRosId(body.rosId)}`
+11 -9
View File
@@ -8,7 +8,7 @@ import {
const internetPathRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/internet-path/settings", async (_req, reply) => {
const s = getInternetPathSettings()
const s = await getInternetPathSettings()
return reply.send({
enabled: s.enabled,
intervalSec: s.intervalSec,
@@ -25,12 +25,12 @@ const internetPathRoutes: FastifyPluginAsyncZod = async (app) => {
intervalSec?: number | string
retentionDays?: number | string
}
const updated = updateInternetPathSettings({
const updated = await updateInternetPathSettings({
enabled: body.enabled,
intervalSec: body.intervalSec == null ? undefined : Math.max(30, Number.parseInt(String(body.intervalSec), 10) || 300),
retentionDays: body.retentionDays == null ? undefined : Math.max(1, Number.parseInt(String(body.retentionDays), 10) || 14),
})
refreshScheduler()
await refreshScheduler()
return reply.send({
ok: true,
settings: {
@@ -45,13 +45,15 @@ const internetPathRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/internet-path/latest", async (_req, reply) => {
const row = getLatestInternetPathSnapshot()
const row = await getLatestInternetPathSnapshot()
if (!row) return reply.send({ snapshot: null })
let payload: unknown = null
try {
payload = JSON.parse(row.payloadJson)
} catch {
payload = null
let payload: unknown = row.payloadJson
if (typeof row.payloadJson === "string") {
try {
payload = JSON.parse(row.payloadJson)
} catch {
payload = null
}
}
return reply.send({
snapshot: payload,
+23 -28
View File
@@ -207,23 +207,21 @@ function calcRouteScore(pingMs: number, dlMbps: number, ulMbps: number, pingWeig
return Math.round(w * pingScore + (1 - w) * speedScore)
}
function getLatestSnapshotLatencyMs(serverId: number): number {
const latest = db
async function getLatestSnapshotLatencyMs(serverId: number): Promise<number> {
const latest = await db
.select()
.from(serverSnapshots)
.where(eq(serverSnapshots.serverId, serverId))
.orderBy(desc(serverSnapshots.polledAt))
.limit(1)
.all()
return latest.length > 0 ? Math.max(1, Math.round(latest[0].latencyMs ?? 100)) : 100
}
function latestTrafficByInterface(serverId: number): Map<string, TrafficSampleRow> {
const rows = db
async function latestTrafficByInterface(serverId: number): Promise<Map<string, { id: number; serverId: number; disabled: boolean; sampledAt: string; interfaceName: string; peerPublicKey: string; rxBytes: number; txBytes: number; rxBps: number; txBps: number; running: boolean; }>> {
const rows = await db
.select()
.from(trafficSamples)
.where(eq(trafficSamples.serverId, serverId))
.all()
const map = new Map<string, TrafficSampleRow>()
for (const row of rows) {
const prev = map.get(row.interfaceName)
@@ -302,14 +300,14 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
const areaMap = buildAreaMap(areas)
const parsed = parseInterfaces(server, ifaceTemplates, areas, instances, areaMap)
const editable = parsed.filter((i) => !i.disabled && !isRefInterfaceName(i.interface))
const localLatencyMs = getLatestSnapshotLatencyMs(server.id)
const latestTraffic = latestTrafficByInterface(server.id)
const localLatencyMs = await getLatestSnapshotLatencyMs(server.id)
const latestTraffic = await latestTrafficByInterface(server.id)
// 1) Сопоставляем remote OSPF router-id -> сервер из каталога (чтобы взять унифицированный ping как в карте /route-optimizer).
const neededRouterIds = new Set(neighbors.map((n) => String(n["router-id"] ?? "")).filter((x) => x.length > 0))
async function buildRouterIdToServerMap(needed: Set<string>): Promise<Map<string, ServerRow["id"]>> {
const enabledServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
const out = new Map<string, ServerRow["id"]>()
const neededLeft = new Set(needed)
@@ -356,11 +354,10 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
}
// 3) Предгружаем uptimeSpeedProbes: оттуда берём и ping, и скорость (единый источник как у карты).
const allSourceProbes = db
const allSourceProbes = (await db
.select()
.from(uptimeSpeedProbes)
.where(eq(uptimeSpeedProbes.srcServerId, server.id))
.all()
.where(eq(uptimeSpeedProbes.srcServerId, server.id)))
.filter((r: SpeedProbeRow) => r.enabled !== false)
const speedProbesByDest = new Map<number, SpeedProbeRow[]>()
for (const p of allSourceProbes) {
@@ -482,8 +479,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
for (const [iface, probe] of assigned.entries()) assignedProbeByIface.set(iface, probe)
}
return editable
.map((iface) => {
return (await Promise.all(editable.map(async (iface) => {
const t = latestTraffic.get(iface.interface)
const speed = normalizeSpeedMbps(t)
@@ -494,7 +490,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
bestProbe?.lastPingRttMs != null
? Math.max(1, Math.round(bestProbe.lastPingRttMs))
: dstServerId != null
? Math.min(995, Math.round(localLatencyMs + getLatestSnapshotLatencyMs(dstServerId)))
? Math.min(995, Math.round(localLatencyMs + await getLatestSnapshotLatencyMs(dstServerId)))
: localLatencyMs
const dlMbps =
@@ -517,8 +513,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
score: calcRouteScore(pingMs, dlMbps, ulMbps, pingWeight),
optimalCost: 0,
}
})
.sort((a, b) => b.score - a.score || a.interface.localeCompare(b.interface))
}))).sort((a, b) => b.score - a.score || a.interface.localeCompare(b.interface))
.map((row, idx) => ({ ...row, optimalCost: (idx + 1) * 10 }))
}
@@ -528,7 +523,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/ospf/neighbors — aggregate OSPF neighbors from ALL enabled servers
app.get("/ospf/neighbors", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const results = await Promise.all(
allServers.map(async (server) => {
@@ -547,7 +542,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/ospf/interfaces — aggregate OSPF interface templates from ALL enabled servers
app.get("/ospf/interfaces", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const results = await Promise.all(
allServers.map(async (server) => {
@@ -566,7 +561,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/ospf/instances — aggregate OSPF instances from ALL enabled servers
app.get("/ospf/instances", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const results = await Promise.all(
allServers.map(async (server) => {
@@ -584,7 +579,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/ospf/all — single round-trip: neighbors + interfaces + instances + BFD
app.get("/ospf/all", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const perServer = await Promise.all(
allServers.map(async (server) => {
@@ -613,7 +608,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/bfd/sessions — BFD sessions only (for direct access)
app.get("/bfd/sessions", async (_req, reply) => {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const results = await Promise.all(
allServers.map(async (server) => {
try {
@@ -631,10 +626,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
const server = (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
@@ -659,10 +654,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
const server = (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
@@ -691,10 +686,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const server = db
const server = (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
+6 -6
View File
@@ -256,14 +256,14 @@ async function mtuDiscover(client: MikrotikClient, address: string, srcIpv4: str
return lines.join("\n")
}
function resolveBtestPeer(remoteHost: string, explicitDstId?: number) {
async function resolveBtestPeer(remoteHost: string, explicitDstId?: number) {
if (explicitDstId != null && Number.isFinite(explicitDstId)) {
const s = db.select().from(servers).where(eq(servers.id, explicitDstId)).limit(1).all()[0]
const s = (await db.select().from(servers).where(eq(servers.id, explicitDstId)).limit(1))[0]
if (s) return s
}
const norm = remoteHost.trim().toLowerCase()
return db.select().from(servers).where(eq(servers.enabled, true)).all()
.find((x) => x.host.trim().toLowerCase() === norm)
const enabled = await db.select().from(servers).where(eq(servers.enabled, true))
return enabled.find((x) => x.host.trim().toLowerCase() === norm)
}
const probesRoutes: FastifyPluginAsyncZod = async (app) => {
@@ -271,7 +271,7 @@ const probesRoutes: FastifyPluginAsyncZod = async (app) => {
const sid = parseServerId(String((req.params as { id?: string }).id ?? ""))
if (sid === null) return reply.status(400).send({ error: "Invalid server id" })
const server = db.select().from(servers).where(eq(servers.id, sid)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, sid)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
const body = req.body as {
@@ -369,7 +369,7 @@ const probesRoutes: FastifyPluginAsyncZod = async (app) => {
case "bandwidth": {
const remote = String(body.bwRemoteAddress ?? "").trim()
if (!remote) return reply.status(400).send({ error: "bwRemoteAddress required" })
const dst = resolveBtestPeer(remote, body.dstServerId)
const dst = await resolveBtestPeer(remote, body.dstServerId)
if (!dst) {
return reply.status(400).send({
error: "Не найден сервер назначения для bandwidth-test: добавьте узел с host = GRE remote или укажите dstServerId",
+13 -14
View File
@@ -80,13 +80,12 @@ function splitGateway(raw: string): { ip: string; name: string } | null {
return { ip, name: name || ip }
}
function mapDbRoutes(serverId: number): RecursiveRouteDto[] {
const rows = db
async function mapDbRoutes(serverId: number): Promise<RecursiveRouteDto[]> {
const rows = await db
.select()
.from(recursiveRoutes)
.where(eq(recursiveRoutes.serverId, serverId))
.orderBy(asc(recursiveRoutes.sortOrder))
.all()
return rows.map((r) => ({
id: String(r.id),
@@ -118,10 +117,10 @@ function toRouterPayload(route: RecursiveRouteDto): Record<string, string> {
}
async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
db.delete(recursiveRoutes).where(eq(recursiveRoutes.serverId, serverId)).run()
await db.delete(recursiveRoutes).where(eq(recursiveRoutes.serverId, serverId))
if (routes.length === 0) return
const now = new Date().toISOString()
db.insert(recursiveRoutes).values(
await db.insert(recursiveRoutes).values(
routes.map((r, i) => ({
serverId,
sortOrder: i,
@@ -138,7 +137,7 @@ async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
createdAt: now,
updatedAt: now,
})),
).run()
)
}
const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
@@ -146,7 +145,7 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
const q = req.query as { serverId?: string | number }
const serverId = parseDbServerId(q.serverId)
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
@@ -176,16 +175,16 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
const q = req.query as { serverId?: string | number }
const serverId = parseDbServerId(q.serverId)
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
return reply.send({ routes: mapDbRoutes(serverId) })
return reply.send({ routes: await mapDbRoutes(serverId) })
})
app.put("/recursive-routes", async (req, reply) => {
const body = req.body as { serverId?: string | number; routes?: RecursiveRouteDto[] }
const serverId = parseDbServerId(body.serverId)
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
await replaceDbRoutes(serverId, body.routes ?? [])
return reply.send({ ok: true })
@@ -195,10 +194,10 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
const body = req.body as { serverId?: string | number } | undefined
const serverId = parseDbServerId(body?.serverId)
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server: ServerRow | undefined = db
const server: ServerRow | undefined = (await db
.select().from(servers)
.where(eq(servers.id, serverId))
.limit(1).all()[0]
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
@@ -231,7 +230,7 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
const body = req.body as { serverId?: string | number } | undefined
const serverId = parseDbServerId(body?.serverId)
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
try {
@@ -243,7 +242,7 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
await client.delete(`/ip/route/${encodeURIComponent(r[".id"])}`)
}
const dbRows = mapDbRoutes(serverId)
const dbRows = await mapDbRoutes(serverId)
for (const route of dbRows) {
await client.post("/ip/route", toRouterPayload(route))
}
+5 -5
View File
@@ -14,7 +14,7 @@ function parseJobKey(raw: string | undefined): typeof JOB_KEYS[number] | null {
const schedulerRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/scheduler/status", async (_req, reply) => {
return reply.send(getSchedulerStatus())
return reply.send(await getSchedulerStatus())
})
app.get("/scheduler/runs", async (req, reply) => {
@@ -25,12 +25,12 @@ const schedulerRoutes: FastifyPluginAsyncZod = async (app) => {
}
const limit = Math.min(200, Math.max(1, Number.parseInt(String(q.limit ?? "50"), 10) || 50))
const offset = Math.max(0, Number.parseInt(String(q.offset ?? "0"), 10) || 0)
return reply.send(listSchedulerRuns({ jobKey: jobKey ?? undefined, limit, offset }))
return reply.send(await listSchedulerRuns({ jobKey: jobKey ?? undefined, limit, offset }))
})
app.post("/scheduler/refresh", async (_req, reply) => {
refreshScheduler()
return reply.send({ ok: true, status: getSchedulerStatus() })
await refreshScheduler()
return reply.send({ ok: true, status: await getSchedulerStatus() })
})
app.post("/scheduler/jobs/:jobKey/run-now", async (req, reply) => {
@@ -38,7 +38,7 @@ const schedulerRoutes: FastifyPluginAsyncZod = async (app) => {
if (!jobKey) return reply.status(400).send({ error: "Некорректный jobKey" })
try {
await runSchedulerJobNow(jobKey)
return reply.send({ ok: true, status: getSchedulerStatus() })
return reply.send({ ok: true, status: await getSchedulerStatus() })
} catch (e) {
const statusCode = (e as Error & { statusCode?: number }).statusCode
if (statusCode === 409) {
+4 -4
View File
@@ -10,7 +10,7 @@ import {
const serversApiPingRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/servers-api-ping/settings", async (_req, reply) => {
const settings = getServersApiPingSettings()
const settings = await getServersApiPingSettings()
const state = getServersRestPingCollectorState()
return reply.send({
enabled: settings.enabled,
@@ -29,11 +29,11 @@ const serversApiPingRoutes: FastifyPluginAsyncZod = async (app) => {
}
const intervalSec =
body.intervalSec == null ? undefined : Math.max(10, Number.parseInt(String(body.intervalSec), 10) || 120)
const updated = updateServersApiPingSettings({
const updated = await updateServersApiPingSettings({
enabled: body.enabled,
intervalSec,
})
refreshScheduler()
await refreshScheduler()
return reply.send({
ok: true,
settings: {
@@ -49,7 +49,7 @@ const serversApiPingRoutes: FastifyPluginAsyncZod = async (app) => {
app.post("/servers-api-ping/collect-now", async (_req, reply) => {
await collectServersRestPingOnce()
scheduleAlertEngineAfterDataCollectors()
const row = getServersApiPingSettings()
const row = await getServersApiPingSettings()
return reply.send({
ok: true,
lastCollectedAt: row.lastCollectedAt ?? null,
+15 -13
View File
@@ -77,13 +77,13 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/servers
app.get("/", async (_req, reply) => {
return reply.send(listServersRead())
return reply.send(await 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 params = req.params as ServerIdParams
const server = getServerRowById(params.id)
const server = await getServerRowById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
const ipv4 = await resolveRosSrcIpv4(server.host)
@@ -93,7 +93,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// GET /api/servers/:id/wan-runtime — DHCP lease + active default route for HomeRouter WAN uplinks
app.get("/:id/wan-runtime", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = getServerReadById(params.id)
const server = await getServerReadById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
const toIp = (raw: string | null | undefined): string | null => {
@@ -141,7 +141,9 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
}
try {
const client = MikrotikClient.fromServer(getServerRowById(params.id)!)
const row = await getServerRowById(params.id)
if (!row) return reply.status(404).send({ error: "Server not found" })
const client = MikrotikClient.fromServer(row)
const isTrue = (v: unknown) => {
const s = String(v ?? "").trim().toLowerCase()
return s === "true" || s === "yes"
@@ -228,13 +230,13 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
// POST /api/servers
app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
return reply.status(201).send(createServer(req.body as ServerCreateRequest))
return reply.status(201).send(await createServer(req.body as ServerCreateRequest))
})
// GET /api/servers/:id
app.get("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = getServerReadById(params.id)
const server = await getServerReadById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
return reply.send(server)
})
@@ -245,25 +247,25 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
{ schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
async (req, reply) => {
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
const existing = await getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
return reply.send(updateServer(params.id, req.body as ServerUpdateRequest))
return reply.send(await updateServer(params.id, req.body as ServerUpdateRequest))
},
)
// DELETE /api/servers/:id
app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
const existing = await getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
deleteServer(params.id)
await deleteServer(params.id)
return reply.status(204).send()
})
// POST /api/servers/:id/poll
app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const existing = getServerReadById(params.id)
const existing = await getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
try {
@@ -281,9 +283,9 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
async (req, reply) => {
const params = req.params as ServerIdParams
const query = req.query as SnapshotsQuery
const existing = getServerReadById(params.id)
const existing = await getServerReadById(params.id)
if (!existing) return reply.status(404).send({ error: "Server not found" })
return reply.send(listServerSnapshots(params.id, query.limit))
return reply.send(await listServerSnapshots(params.id, query.limit))
},
)
}
+12 -10
View File
@@ -12,22 +12,24 @@ import {
} from "../db/schema.js"
import { listUsers } from "../modules/users/service/users-service.js"
function tableCount(table: typeof servers | typeof filterRules | typeof uptimeProbes | typeof uptimeSpeedProbes | typeof recursiveRoutes): number {
return db.select({ n: count() }).from(table).all()[0]?.n ?? 0
async function tableCount(table: typeof servers | typeof filterRules | typeof uptimeProbes | typeof uptimeSpeedProbes | typeof recursiveRoutes): Promise<number> {
const rows = await db.select({ n: count() }).from(table)
return rows[0]?.n ?? 0
}
const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/sidebar-counts", async (_req, reply) => {
const serversTotal = tableCount(servers)
const filterRulesTotal = tableCount(filterRules)
const uptimeProbesTotal = tableCount(uptimeProbes)
const uptimeSpeedProbesTotal = tableCount(uptimeSpeedProbes)
const recursiveRoutesTotal = tableCount(recursiveRoutes)
const [certificatesTotal, wireguardTotal] = await Promise.all([
listCertificatesFromServers().then((res) => res.certificates.length),
const serversTotal = await tableCount(servers)
const filterRulesTotal = await tableCount(filterRules)
const uptimeProbesTotal = await tableCount(uptimeProbes)
const uptimeSpeedProbesTotal = await tableCount(uptimeSpeedProbes)
const recursiveRoutesTotal = await tableCount(recursiveRoutes)
const [certRes, wireguardTotal] = await Promise.all([
listCertificatesFromServers(),
countWireGuardInterfaces().catch(() => 0),
])
const usersTotal = listUsers().length
const certificatesTotal = certRes.certificates.length
const usersTotal = (await listUsers()).length
return reply.send({
servers: serversTotal,
+4 -4
View File
@@ -18,7 +18,7 @@ const systemDatabaseRoutes: FastifyPluginAsync = async (app) => {
app.get("/system/database/backup", async (_req, reply) => {
try {
const { filename, buffer } = await exportSystemDatabaseBackup()
appendEvent({
await appendEvent({
level: "info",
eventType: "system.database.backup",
sourceModule: "system",
@@ -44,16 +44,16 @@ const systemDatabaseRoutes: FastifyPluginAsync = async (app) => {
app.post("/system/database/restore", async (req, reply) => {
const body = req.body
if (!Buffer.isBuffer(body) || body.length === 0) {
return reply.status(400).send({ error: "Ожидается тело запроса с файлом SQLite" })
return reply.status(400).send({ error: "Ожидается тело запроса с файлом pg_dump (custom, PGDMP)" })
}
try {
await restoreSystemDatabaseBackup(body)
appendEvent({
await appendEvent({
level: "warning",
eventType: "system.database.restore",
sourceModule: "system",
title: "Восстановлена база приложения",
message: "Данные SQLite заменены из загруженного файла",
message: "Данные PostgreSQL заменены из загруженного дампа",
entityType: "system_database",
entityId: "restore",
payload: {
+16 -16
View File
@@ -94,7 +94,7 @@ function analyticsQuery(req: FastifyRequest) {
async function sendFlowTalkers(req: FastifyRequest, reply: FastifyReply) {
const q = req.query as { range?: string }
return reply.send(listFlowTalkers(rangeToMinutes(q.range)))
return reply.send(await listFlowTalkers(rangeToMinutes(q.range)))
}
function requestPublicHost(req: FastifyRequest): string {
@@ -145,7 +145,7 @@ function sleep(ms: number, signal: AbortSignal): Promise<void> {
const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/traffic/flow/settings", async (_req, reply) => {
return reply.send(toTrafficFlowSettingsDto(getFlowListenerState()))
return reply.send(await toTrafficFlowSettingsDto(getFlowListenerState()))
})
app.put("/traffic/flow/settings", async (req, reply) => {
@@ -153,31 +153,31 @@ const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
updateTrafficFlowSettings(parsed.data)
startTrafficFlowListener()
return reply.send({ ok: true, settings: toTrafficFlowSettingsDto(getFlowListenerState()) })
await updateTrafficFlowSettings(parsed.data)
await startTrafficFlowListener()
return reply.send({ ok: true, settings: await toTrafficFlowSettingsDto(getFlowListenerState()) })
})
app.post("/traffic/flow/settings/generate-keys", async (_req, reply) => {
const result = ensureHostKeys()
const result = await ensureHostKeys()
return reply.send({
ok: true,
created: result.created,
publicKey: result.publicKey,
settings: toTrafficFlowSettingsDto(getFlowListenerState()),
settings: await toTrafficFlowSettingsDto(getFlowListenerState()),
})
})
app.get("/traffic/flow/host-files", async (_req, reply) => {
const row = getTrafficFlowSettingsRow()
if (!row.hostPrivateKey) ensureHostKeys()
return reply.send({ files: listTrafficFlowHostFiles() })
const row = await getTrafficFlowSettingsRow()
if (!row.hostPrivateKey) await ensureHostKeys()
return reply.send({ files: await listTrafficFlowHostFiles() })
})
app.post("/traffic/flow/purge", async (_req, reply) => {
try {
const result = await purgeTrafficFlowStore()
appendEvent({
await appendEvent({
level: "warning",
eventType: "traffic.flow.purge",
sourceModule: "traffic",
@@ -211,20 +211,20 @@ const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/traffic/flow/exporters", async (req, reply) => {
const q = req.query as { range?: string }
return reply.send(listFlowExporters(rangeToMinutes(q.range)))
return reply.send(await listFlowExporters(rangeToMinutes(q.range)))
})
app.get("/traffic/flow/clients", async (req, reply) => {
const q = req.query as { range?: string }
return reply.send(listFlowClients(rangeToMinutes(q.range)))
return reply.send(await listFlowClients(rangeToMinutes(q.range)))
})
app.get("/traffic/flow/analytics", async (req, reply) => {
return reply.send(buildFlowAnalytics(analyticsQuery(req)))
return reply.send(await buildFlowAnalytics(analyticsQuery(req)))
})
app.get("/traffic/flow/map-hops", async (req, reply) => {
return reply.send(buildFlowMapHops(analyticsQuery(req)))
return reply.send(await buildFlowMapHops(analyticsQuery(req)))
})
app.get("/traffic/flow/monthly", async (req, reply) => {
@@ -233,7 +233,7 @@ const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
const month = /^\d{4}-\d{2}$/.test(q.month ?? "")
? (q.month as string)
: `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`
return reply.send(getFlowMonthly(month, parseId(q.serverId)))
return reply.send(await getFlowMonthly(month, parseId(q.serverId)))
})
app.get("/traffic/flow/live", async (req, reply) => {
+26 -27
View File
@@ -57,14 +57,13 @@ interface TrafficInterfaceDto {
const LIVE_TICK_MS = 1500
const LIVE_ROS_TIMEOUT_MS = 4000
function latestSnapshot(serverId: number): SnapshotRow | undefined {
return db
async function latestSnapshot(serverId: number) {
return (await db
.select()
.from(serverSnapshots)
.where(eq(serverSnapshots.serverId, serverId))
.orderBy(desc(serverSnapshots.polledAt))
.limit(1)
.all()[0]
.limit(1))[0]
}
function rangeToMinutes(range: string | undefined): number {
@@ -114,8 +113,8 @@ function buildServerTraffic(
}
}
function snapshotStatus(serverId: number): TrafficServerDto["status"] {
const snap = latestSnapshot(serverId)
async function snapshotStatus(serverId: number): Promise<TrafficServerDto["status"]> {
const snap = await latestSnapshot(serverId)
return snap?.status === "offline" ? "offline" : (snap?.status === "online" ? "online" : "degraded")
}
@@ -222,8 +221,8 @@ async function monitorTrafficOnce(
const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/traffic/settings", async (_req, reply) => {
const settings = getTrafficSettings()
const state = getTrafficCollectorState()
const settings = await getTrafficSettings()
const state = await getTrafficCollectorState()
return reply.send({
enabled: settings.enabled,
intervalSec: settings.intervalSec,
@@ -243,12 +242,12 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
}
const intervalSec = body.intervalSec == null ? undefined : Math.max(5, Number.parseInt(String(body.intervalSec), 10) || 30)
const retentionDays = body.retentionDays == null ? undefined : Math.max(1, Number.parseInt(String(body.retentionDays), 10) || 14)
const updated = updateTrafficSettings({
const updated = await updateTrafficSettings({
enabled: body.enabled,
intervalSec,
retentionDays,
})
refreshScheduler()
await refreshScheduler()
return reply.send({
ok: true,
settings: {
@@ -266,8 +265,8 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
try {
await collectTrafficOnce()
scheduleAlertEngineAfterDataCollectors()
const updated = getTrafficSettings()
appendEvent({
const updated = await getTrafficSettings()
await appendEvent({
level: "info",
eventType: "traffic.collect.manual.ok",
sourceModule: "traffic",
@@ -285,7 +284,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
})
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
appendEvent({
await appendEvent({
level: "critical",
eventType: "traffic.collect.manual.failed",
sourceModule: "traffic",
@@ -302,11 +301,11 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const rangeEndMs = Date.now()
const rangeStartMs = rangeEndMs - minutes * 60_000
const sinceIso = new Date(rangeStartMs).toISOString()
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const data = allServers.map((s): TrafficServerDto => {
const rows = readServerSamplesInRange(s.id, sinceIso)
return buildServerTraffic(s, snapshotStatus(s.id), rows, rangeStartMs, rangeEndMs)
})
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
const data = await Promise.all(allServers.map(async (s): Promise<TrafficServerDto> => {
const rows = await readServerSamplesInRange(s.id, sinceIso)
return buildServerTraffic(s, await snapshotStatus(s.id), rows, rangeStartMs, rangeEndMs)
}))
return reply.send({ servers: data })
})
@@ -314,7 +313,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/traffic/servers/:id/live", async (req, reply) => {
const p = req.params as { id?: string | number }
const q = req.query as { iface?: string }
const server = getEnabledServerById(p.id ?? "")
const server = await getEnabledServerById(p.id ?? "")
if (!server || !server.enabled) return reply.status(404).send({ error: "Server not found" })
const onlyInterface = q.iface && q.iface !== "__all__" ? q.iface : undefined
@@ -375,12 +374,12 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const serverId = Number.parseInt(String(p.id ?? ""), 10)
if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "id is required" })
const allRows = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()
const allRows = await db.select().from(servers).where(eq(servers.id, serverId)).limit(1)
const server = allRows[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
const sinceIso = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
const rows = readServerSamplesInRange(serverId, sinceIso)
const rows = await readServerSamplesInRange(serverId, sinceIso)
const byIface = new Map<string, typeof rows>()
for (const r of rows) {
if (isLoopbackName(r.interfaceName)) continue
@@ -413,17 +412,17 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const q = req.query as { range?: string; iface?: string }
const serverId = Number.parseInt(String(p.id ?? ""), 10)
if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "id is required" })
const server = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const server = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
const minutes = rangeToMinutes(q.range)
const rangeEndMs = Date.now()
const rangeStartMs = rangeEndMs - minutes * 60_000
const sinceIso = new Date(rangeStartMs).toISOString()
const rows = readServerSamplesInRange(server.id, sinceIso)
const rows = await readServerSamplesInRange(server.id, sinceIso)
const data = buildServerTraffic(
server,
snapshotStatus(server.id),
await snapshotStatus(server.id),
rows,
rangeStartMs,
rangeEndMs,
@@ -437,7 +436,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const minutes = rangeToMinutes(q.range)
const rangeEndMs = Date.now()
const rangeStartMs = rangeEndMs - minutes * 60_000
return reply.send({ users: buildUserTrafficList(rangeStartMs, rangeEndMs) })
return reply.send({ users: await buildUserTrafficList(rangeStartMs, rangeEndMs) })
})
app.get("/traffic/users/:id", async (req, reply) => {
@@ -448,7 +447,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const minutes = rangeToMinutes(q.range)
const rangeEndMs = Date.now()
const rangeStartMs = rangeEndMs - minutes * 60_000
const users = buildUserTrafficList(rangeStartMs, rangeEndMs)
const users = await buildUserTrafficList(rangeStartMs, rangeEndMs)
const user = users.find((u) => u.id === id)
if (!user) return reply.status(404).send({ error: "Пользователь не найден" })
return reply.send({ user })
@@ -459,7 +458,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
const minutes = rangeToMinutes(q.range)
const rangeEndMs = Date.now()
const rangeStartMs = rangeEndMs - minutes * 60_000
return reply.send({ interfaces: buildBoundInterfaceTraffic(rangeStartMs, rangeEndMs) })
return reply.send({ interfaces: await buildBoundInterfaceTraffic(rangeStartMs, rangeEndMs) })
})
}
+33 -33
View File
@@ -36,8 +36,8 @@ function toSeries(values: number[], target = 40): number[] {
return [...Array(target - values.length).fill(values[0] ?? 0), ...values]
}
/** Сэмпл ресурсов из SQLite (readResourceSamplesSince). */
type ResourceSampleRow = ReturnType<typeof readResourceSamplesSince>[number]
/** Сэмпл ресурсов из PostgreSQL (readResourceSamplesSince). */
type ResourceSampleRow = Awaited<ReturnType<typeof readResourceSamplesSince>>[number]
/**
* Последний сэмпл по времени может быть offline после сбоя опроса — тогда UI «пустой».
@@ -67,7 +67,7 @@ function pickResourceDisplayRow(rows: ResourceSampleRow[], maxGapMs: number): {
const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/uptime/settings", async (_req, reply) => {
const s = readUptimeSettings()
const s = await readUptimeSettings()
return reply.send({
enabled: s.enabled,
resourcesEnabled: s.resourcesEnabled ?? s.enabled,
@@ -82,7 +82,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
lastCollectedAt: s.lastCollectedAt ?? null,
lastDurationMs: s.lastDurationMs ?? null,
lastError: s.lastError || null,
scheduler: getSchedulerStatus(),
scheduler: await getSchedulerStatus(),
})
})
@@ -97,7 +97,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
speedIntervalSec?: number | string
retentionDays?: number | string
}
const updated = updateUptimeSettings({
const updated = await updateUptimeSettings({
enabled: body.enabled,
resourcesEnabled: body.resourcesEnabled,
pingEnabled: body.pingEnabled,
@@ -107,7 +107,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
speedIntervalSec: body.speedIntervalSec == null ? undefined : Math.max(10, Number.parseInt(String(body.speedIntervalSec), 10) || 60),
retentionDays: body.retentionDays == null ? undefined : Math.max(1, Number.parseInt(String(body.retentionDays), 10) || 14),
})
refreshScheduler()
await refreshScheduler()
return reply.send({
ok: true,
settings: {
@@ -122,7 +122,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
lastCollectedAt: updated.lastCollectedAt ?? null,
lastDurationMs: updated.lastDurationMs ?? null,
lastError: updated.lastError || null,
scheduler: getSchedulerStatus(),
scheduler: await getSchedulerStatus(),
},
})
})
@@ -130,7 +130,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
app.post("/uptime/collect-now", async (_req, reply) => {
await collectUptimeOnce()
scheduleAlertEngineAfterDataCollectors()
const s = readUptimeSettings()
const s = await readUptimeSettings()
return reply.send({
ok: true,
lastCollectedAt: s.lastCollectedAt ?? null,
@@ -158,7 +158,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/uptime/probes", async (_req, reply) => {
const rows = readProbeRows()
const rows = await readProbeRows()
return reply.send({
probes: rows.map((p) => ({
id: p.id,
@@ -178,7 +178,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/uptime/speed-probes", async (_req, reply) => {
const rows = db.select().from(uptimeSpeedProbes).orderBy(asc(uptimeSpeedProbes.sortOrder)).all()
const rows = await db.select().from(uptimeSpeedProbes).orderBy(asc(uptimeSpeedProbes.sortOrder))
return reply.send({
probes: rows.map((r) => ({
id: r.id,
@@ -204,7 +204,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/uptime/speed-test/runs", async (_req, reply) => {
const rows = db.select().from(uptimeSpeedTestRuns).orderBy(desc(uptimeSpeedTestRuns.createdAt)).limit(200).all()
const rows = await db.select().from(uptimeSpeedTestRuns).orderBy(desc(uptimeSpeedTestRuns.createdAt)).limit(200)
return reply.send({
runs: rows.map((r) => ({
id: r.id,
@@ -263,12 +263,12 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
.filter((p) => Number.isFinite(p.srcServerId) && Number.isFinite(p.dstServerId) && p.srcServerId !== p.dstServerId)
const now = new Date().toISOString()
const existing = db.select({ id: uptimeSpeedProbes.id }).from(uptimeSpeedProbes).all()
const existing = await db.select({ id: uptimeSpeedProbes.id }).from(uptimeSpeedProbes)
const nextIds = new Set(normalized.map((p) => p.id))
for (const row of existing) {
if (nextIds.has(row.id)) continue
db.delete(uptimeSpeedProbes).where(eq(uptimeSpeedProbes.id, row.id)).run()
await db.delete(uptimeSpeedProbes).where(eq(uptimeSpeedProbes.id, row.id))
}
for (let i = 0; i < normalized.length; i += 1) {
@@ -285,15 +285,15 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
sortOrder: i,
updatedAt: now,
}
const updated = db.update(uptimeSpeedProbes).set(patch).where(eq(uptimeSpeedProbes.id, p.id)).run()
if ((updated.changes ?? 0) > 0) continue
db.insert(uptimeSpeedProbes).values({
const updated = await db.update(uptimeSpeedProbes).set(patch).where(eq(uptimeSpeedProbes.id, p.id)).returning({ id: uptimeSpeedProbes.id })
if (updated.length > 0) continue
await db.insert(uptimeSpeedProbes).values({
id: p.id,
...patch,
createdAt: now,
}).run()
})
}
refreshScheduler()
await refreshScheduler()
return reply.send({ ok: true })
})
@@ -326,8 +326,8 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
: Number.parseInt(String(p.intervalSec), 10),
}))
.filter((p) => Number.isFinite(p.srcServerId) && p.name && p.target)
replaceProbes(normalized)
refreshScheduler()
await replaceProbes(normalized)
await refreshScheduler()
return reply.send({ ok: true })
})
@@ -339,7 +339,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
if (typeof body.showOnDashboard !== "boolean") {
return reply.status(400).send({ error: "showOnDashboard boolean required" })
}
const ok = updateProbeShowOnDashboard(probeId, body.showOnDashboard)
const ok = await updateProbeShowOnDashboard(probeId, body.showOnDashboard)
if (!ok) return reply.status(404).send({ error: "Probe not found" })
return reply.send({ ok: true })
})
@@ -348,7 +348,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
const params = req.params as { id?: string | number }
const serverId = Number.parseInt(String(params.id ?? ""), 10)
if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "Invalid server id" })
const srv = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const srv = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!srv) return reply.status(404).send({ error: "Server not found" })
try {
const rows = await MikrotikClient.fromServer(srv).getInterfaces()
@@ -386,8 +386,8 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Invalid src/dst server id" })
}
if (srcId === dstId) return reply.status(400).send({ error: "Source and destination must be different" })
const src = db.select().from(servers).where(eq(servers.id, srcId)).limit(1).all()[0]
const dst = db.select().from(servers).where(eq(servers.id, dstId)).limit(1).all()[0]
const src = (await db.select().from(servers).where(eq(servers.id, srcId)).limit(1))[0]
const dst = (await db.select().from(servers).where(eq(servers.id, dstId)).limit(1))[0]
if (!src || !dst) return reply.status(404).send({ error: "Server not found" })
try {
const result = await runBandwidthSpeedTest({
@@ -404,7 +404,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.send({ ok: true, result })
} catch (e) {
const raw = e instanceof Error ? e.message : "Bandwidth test failed"
recordSpeedTestFailure({
await recordSpeedTestFailure({
probeId: String(body.probeId ?? "").trim() || undefined,
runId: String(body.runId ?? "").trim() || undefined,
srcServerId: srcId,
@@ -430,7 +430,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
const params = req.params as { id?: string | number }
const serverId = Number.parseInt(String(params.id ?? ""), 10)
if (!Number.isFinite(serverId)) return reply.status(400).send({ error: "Invalid server id" })
const srv = db.select().from(servers).where(eq(servers.id, serverId)).limit(1).all()[0]
const srv = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
if (!srv) return reply.status(404).send({ error: "Server not found" })
try {
const client = MikrotikClient.fromServer(srv)
@@ -473,10 +473,10 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
const minutes = rangeToMinutes(q.range)
const sinceIso = new Date(Date.now() - minutes * 60_000).toISOString()
const allServers = db.select().from(servers).all()
const probeRows = readProbeRows()
const probeSamples = readProbeSamplesSince(sinceIso)
const uptimeCfg = readUptimeSettings()
const allServers = await db.select().from(servers)
const probeRows = await readProbeRows()
const probeSamples = await readProbeSamplesSince(sinceIso)
const uptimeCfg = await readUptimeSettings()
const intervalSec = Math.max(15, uptimeCfg.intervalSec ?? 300)
/** Допустимый разрыв между последней попыткой и последним «хорошим» сэмплом (не показывать данные часовой давности). */
const resourceFallbackMaxGapMs = Math.min(2 * 3600 * 1000, Math.max(10 * 60 * 1000, intervalSec * 6 * 1000))
@@ -501,8 +501,8 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
}
})
const resources = allServers.map((s) => {
const rows = readResourceSamplesSince(sinceIso, s.id)
const resources = await Promise.all(allServers.map(async (s) => {
const rows = await readResourceSamplesSince(sinceIso, s.id)
const { row: pick, hasData } = pickResourceDisplayRow(rows, resourceFallbackMaxGapMs)
const cpuHistory = toSeries(rows.map((r) => r.cpuLoad), 40)
return {
@@ -518,7 +518,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
boardName: pick?.boardName || "RouterBOARD",
temp: undefined as number | undefined,
}
})
}))
return reply.send({ probes, resources })
})
+7 -7
View File
@@ -31,7 +31,7 @@ function sendServiceError(reply: { status: (c: number) => { send: (b: unknown) =
const usersRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/users", async (_req, reply) => {
return reply.send({ users: listUsers() })
return reply.send({ users: await listUsers() })
})
app.get("/users/interface-catalog", {
@@ -49,7 +49,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
schema: { params: appUserIdParamSchema },
}, async (req, reply) => {
const { id } = req.params as { id: string }
const user = getUserById(id)
const user = await getUserById(id)
if (!user) return reply.status(404).send({ error: "Пользователь не найден" })
return reply.send({ user })
})
@@ -58,7 +58,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
schema: { body: appUserCreateSchema },
}, async (req, reply) => {
try {
const user = createUser(req.body as AppUserCreate)
const user = await createUser(req.body as AppUserCreate)
return reply.status(201).send({ user })
} catch (err) {
return sendServiceError(reply, err)
@@ -70,7 +70,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
}, async (req, reply) => {
const { id } = req.params as { id: string }
try {
const user = updateUser(id, req.body as AppUserUpdate)
const user = await updateUser(id, req.body as AppUserUpdate)
return reply.send({ user })
} catch (err) {
return sendServiceError(reply, err)
@@ -82,7 +82,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
}, async (req, reply) => {
const { id } = req.params as { id: string }
try {
deleteUser(id)
await deleteUser(id)
return reply.status(204).send()
} catch (err) {
return sendServiceError(reply, err)
@@ -94,7 +94,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
}, async (req, reply) => {
const { id } = req.params as { id: string }
try {
const binding = addBinding(id, req.body as UserBindingCreate)
const binding = await addBinding(id, req.body as UserBindingCreate)
return reply.status(201).send({ binding })
} catch (err) {
return sendServiceError(reply, err)
@@ -106,7 +106,7 @@ const usersRoutes: FastifyPluginAsyncZod = async (app) => {
}, async (req, reply) => {
const { id, bindingId } = req.params as { id: string; bindingId: string }
try {
removeBinding(id, bindingId)
await removeBinding(id, bindingId)
return reply.status(204).send()
} catch (err) {
return sendServiceError(reply, err)
+9 -9
View File
@@ -154,7 +154,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = getEnabledServerById(body.serverId)
const server = await getEnabledServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
@@ -194,7 +194,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const server = getEnabledServerById(serverIdParam(serverId))
const server = await getEnabledServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const d = parsed.data
@@ -219,7 +219,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
app.delete("/wireguard/interfaces/:serverId/:rosId", async (req, reply) => {
const { serverId, rosId } = req.params as { serverId: string; rosId: string }
const server = getEnabledServerById(serverIdParam(serverId))
const server = await getEnabledServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
try {
@@ -237,7 +237,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = getEnabledServerById(body.serverId)
const server = await getEnabledServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
try {
@@ -255,7 +255,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const server = getEnabledServerById(serverIdParam(serverId))
const server = await getEnabledServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const d = parsed.data
const client = MikrotikClient.fromServer(server)
@@ -286,7 +286,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
app.delete("/wireguard/peers/:serverId/:rosId", async (req, reply) => {
const { serverId, rosId } = req.params as { serverId: string; rosId: string }
const server = getEnabledServerById(serverIdParam(serverId))
const server = await getEnabledServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
try {
@@ -315,7 +315,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.send({ dryRun: true, preview })
}
const server = getEnabledServerById(body.serverId)
const server = await getEnabledServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
try {
@@ -333,14 +333,14 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const body = parsed.data
const server = getEnabledServerById(body.serverId)
const server = await getEnabledServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const list = await listWireGuardInterfaces({
serverId: String(server.id),
includePrivateKey: body.includePrivateKey === true,
})
const iface = findIface(list.interfaces, String(server.id), body.interfaceName)
const iface = await findIface(list.interfaces, String(server.id), body.interfaceName)
if (!iface) return reply.status(404).send({ error: "Интерфейс не найден" })
if (body.format === "rsc") {