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
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:
1 parent
0e5fb065e2
commit
ec43591a99
128 files changed
+4304
-3639
No files matched your search
@@ -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)."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" })
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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" })
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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
@@ -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" })
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in new issue
Block a user