chore: synchronize pending app/backend updates and repository hygiene
Includes current frontend and backend work in progress and removes generated artifacts from tracking to keep the repository clean for дальнейшая разработка. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
bdb9b72fac
commit
5f31bb47fb
81 files changed
+11976
-1239
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import http from "node:http"
|
||||
import https from "node:https"
|
||||
import type { Server } from "../db/schema.js"
|
||||
import { parseRosDataSizeBytes } from "./ros-metric-parse.js"
|
||||
import type {
|
||||
RosIdentity, RosInterface, RosIpAddress, RosResource,
|
||||
RosBgpSession,
|
||||
@@ -199,6 +200,76 @@ function rosDelete(
|
||||
})
|
||||
}
|
||||
|
||||
function rosPatch(
|
||||
params: MikrotikConnectParams,
|
||||
path: string,
|
||||
body: Record<string, string>,
|
||||
timeoutMs: number,
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const basePath = params.apiPath ?? "/rest"
|
||||
const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")
|
||||
const payload = JSON.stringify(body)
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: params.host,
|
||||
port: params.port,
|
||||
path: basePath + path,
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(payload),
|
||||
},
|
||||
rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
|
||||
}
|
||||
|
||||
const lib = params.useSsl ? https : http
|
||||
const timer = setTimeout(() => {
|
||||
req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
|
||||
}, timeoutMs)
|
||||
|
||||
const req = lib.request(options, (res) => {
|
||||
let buf = ""
|
||||
res.setEncoding("utf8")
|
||||
res.on("data", (chunk: string) => { buf += chunk })
|
||||
res.on("end", () => {
|
||||
clearTimeout(timer)
|
||||
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(new MikrotikError(res.statusCode ?? 0, path, buf)); return
|
||||
}
|
||||
if (buf.trim() === "") {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
try { resolve(JSON.parse(buf)) } catch { resolve({}) }
|
||||
})
|
||||
})
|
||||
|
||||
req.on("error", (err) => {
|
||||
clearTimeout(timer)
|
||||
reject(err)
|
||||
})
|
||||
req.write(payload)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* RouterOS REST для одиночных меню (`/system/resource`, `/system/identity`) отдаёт JSON-массив из одного объекта `[{...}]`,
|
||||
* а не сам объект. Без распаковки поля читаются с массива → всегда undefined и в БД уходят нули при «успехе».
|
||||
* @see https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
|
||||
*/
|
||||
function unwrapRestSingle<T extends object>(raw: unknown): T {
|
||||
if (Array.isArray(raw)) {
|
||||
const first = raw[0]
|
||||
if (first && typeof first === "object") return first as T
|
||||
throw new Error("RouterOS REST: пустой массив вместо записи")
|
||||
}
|
||||
if (raw && typeof raw === "object") return raw as T
|
||||
throw new Error("RouterOS REST: неожиданное тело ответа")
|
||||
}
|
||||
|
||||
// ── MikrotikClient ─────────────────────────────────────────────────────────────
|
||||
|
||||
export class MikrotikClient {
|
||||
@@ -228,14 +299,43 @@ export class MikrotikClient {
|
||||
return rosDelete(this.params, path, timeoutMs)
|
||||
}
|
||||
|
||||
async patch<T>(path: string, body: Record<string, string>, timeoutMs = 15_000): Promise<T> {
|
||||
return rosPatch(this.params, path, body, timeoutMs) as Promise<T>
|
||||
}
|
||||
|
||||
// ── typed helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
async getIdentity(): Promise<RosIdentity> {
|
||||
return this.get<RosIdentity>("/system/identity")
|
||||
const raw = await this.get<unknown>("/system/identity")
|
||||
return unwrapRestSingle<RosIdentity>(raw)
|
||||
}
|
||||
|
||||
async getResource(): Promise<RosResource> {
|
||||
return this.get<RosResource>("/system/resource")
|
||||
const raw = await this.get<unknown>("/system/resource")
|
||||
if (Array.isArray(raw)) {
|
||||
if (raw.length === 0) throw new Error("RouterOS REST: пустой массив вместо записи /system/resource")
|
||||
if (raw.length === 1) return unwrapRestSingle<RosResource>(raw[0])
|
||||
/** Несколько объектов: взять строку с total-memory (агрегат), а не дочернюю запись с «чужим» uptime. */
|
||||
const objs = raw.filter((x): x is Record<string, unknown> => x != null && typeof x === "object" && !Array.isArray(x))
|
||||
/** На части железа несколько строк; «первая с непустым total-memory» может парситься в 0 — берём запись с максимальным RAM. */
|
||||
let best: Record<string, unknown> | undefined
|
||||
let bestBytes = -1
|
||||
for (const o of objs) {
|
||||
const tm = parseRosDataSizeBytes(o["total-memory"])
|
||||
const fm = parseRosDataSizeBytes(o["free-memory"])
|
||||
const score = tm > 0 ? tm : fm
|
||||
if (score > bestBytes) {
|
||||
bestBytes = score
|
||||
best = o
|
||||
}
|
||||
}
|
||||
const main = objs.find((o) => {
|
||||
const tm = o["total-memory"]
|
||||
return tm != null && String(tm).trim() !== ""
|
||||
})
|
||||
return ((bestBytes > 0 ? best : undefined) ?? main ?? objs[0]) as unknown as RosResource
|
||||
}
|
||||
return unwrapRestSingle<RosResource>(raw)
|
||||
}
|
||||
|
||||
async getInterfaces(): Promise<RosInterface[]> {
|
||||
@@ -277,6 +377,11 @@ export class MikrotikClient {
|
||||
return this.get<RosBfdSession[]>("/routing/bfd/session")
|
||||
}
|
||||
|
||||
async setOspfInterfaceTemplateCost(templateId: string, cost: number): Promise<void> {
|
||||
const safeId = templateId.startsWith("*") ? templateId : `*${templateId.replace(/^\*/, "")}`
|
||||
await this.patch(`/routing/ospf/interface-template/${encodeURIComponent(safeId)}`, { cost: String(cost) })
|
||||
}
|
||||
|
||||
// ── extra endpoints for exec route ────────────────────────────────────────
|
||||
|
||||
async getIpRoutes(): Promise<RosIpRoute[]> {
|
||||
|
||||
Reference in new issue
Block a user