feat: enhance backups page with live data loading and backup job management
Implemented live data fetching for servers and backups on the backups page, replacing static initial data. Added functionality for manual backup creation and job status tracking, including error handling and UI updates. Updated the network map layout to improve node prioritization and visual representation of server roles. Also, registered new backups API routes in the backend for improved data handling.
This commit is contained in:
@@ -430,6 +430,118 @@ export class MikrotikClient {
|
||||
}
|
||||
return this.post<Array<Record<string, string>>>("/tool/bandwidth-test", body, 30_000)
|
||||
}
|
||||
|
||||
async exportConfigScript(): Promise<string> {
|
||||
const raw = await this.post<unknown>("/console/export", {}, 30_000)
|
||||
|
||||
const asText = (v: unknown): string | null => {
|
||||
if (typeof v === "string") return v.trim().length > 0 ? v : null
|
||||
if (Array.isArray(v)) {
|
||||
const parts = v
|
||||
.map((item) => asText(item))
|
||||
.filter((s): s is string => typeof s === "string" && s.length > 0)
|
||||
return parts.length > 0 ? parts.join("\n") : null
|
||||
}
|
||||
if (v && typeof v === "object") {
|
||||
const rec = v as Record<string, unknown>
|
||||
const direct =
|
||||
asText(rec.output) ??
|
||||
asText(rec.stdout) ??
|
||||
asText(rec.data) ??
|
||||
asText(rec.ret) ??
|
||||
asText(rec["!re"])
|
||||
if (direct) return direct
|
||||
const serialized = JSON.stringify(rec, null, 2)
|
||||
return serialized.length > 2 ? serialized : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const txt = asText(raw)
|
||||
if (txt && txt.trim().length > 0) return txt
|
||||
|
||||
// Fallback: на части RouterOS /console/export возвращает пустое тело.
|
||||
// Тогда строим .rsc-скрипт из основных read-only разделов REST.
|
||||
return this.buildSyntheticExportScript()
|
||||
}
|
||||
|
||||
private async buildSyntheticExportScript(): Promise<string> {
|
||||
const now = new Date().toISOString()
|
||||
const lines: string[] = [
|
||||
"# synthetic export generated by MikrotikManager",
|
||||
`# generated-at: ${now}`,
|
||||
"",
|
||||
]
|
||||
|
||||
const identity = await this.getIdentity().catch(() => null)
|
||||
if (identity?.name) {
|
||||
lines.push("/system identity")
|
||||
lines.push(`set name="${identity.name.replace(/"/g, "\\\"")}"`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
const interfaces = await this.getInterfaces().catch(() => [])
|
||||
if (interfaces.length > 0) {
|
||||
lines.push("/interface")
|
||||
for (const i of interfaces) {
|
||||
if (!i.name) continue
|
||||
const mtu = i["actual-mtu"] ?? i.mtu
|
||||
const parts = [
|
||||
`name="${String(i.name).replace(/"/g, "\\\"")}"`,
|
||||
mtu ? `mtu=${mtu}` : null,
|
||||
i.disabled === "true" ? "disabled=yes" : "disabled=no",
|
||||
].filter((v): v is string => typeof v === "string")
|
||||
lines.push(`:put "interface ${parts.join(" ")}"`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
const addrs = await this.getIpAddresses().catch(() => [])
|
||||
if (addrs.length > 0) {
|
||||
lines.push("/ip address")
|
||||
for (const a of addrs) {
|
||||
if (!a.address || !a.interface) continue
|
||||
const comment = a.comment ? ` comment="${String(a.comment).replace(/"/g, "\\\"")}"` : ""
|
||||
lines.push(`add address=${a.address} interface="${String(a.interface).replace(/"/g, "\\\"")}"${comment}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
const routes = await this.getIpRoutes().catch(() => [])
|
||||
if (routes.length > 0) {
|
||||
lines.push("/ip route")
|
||||
for (const r of routes) {
|
||||
const dst = r["dst-address"]
|
||||
const gw = r["gateway"]
|
||||
if (!dst || !gw) continue
|
||||
const distance = r.distance ? ` distance=${r.distance}` : ""
|
||||
lines.push(`add dst-address=${dst} gateway=${gw}${distance}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
const firewall = await this.getFirewallFilters().catch(() => [])
|
||||
if (firewall.length > 0) {
|
||||
lines.push("/ip firewall filter")
|
||||
for (const f of firewall) {
|
||||
if (!f.chain || !f.action) continue
|
||||
const parts = [`chain=${f.chain}`, `action=${f.action}`]
|
||||
if (f.protocol) parts.push(`protocol=${f.protocol}`)
|
||||
if (f["src-address"]) parts.push(`src-address=${f["src-address"]}`)
|
||||
if (f["dst-address"]) parts.push(`dst-address=${f["dst-address"]}`)
|
||||
if (f["dst-port"]) parts.push(`dst-port=${f["dst-port"]}`)
|
||||
if (f["src-port"]) parts.push(`src-port=${f["src-port"]}`)
|
||||
if (f.disabled === "true") parts.push("disabled=yes")
|
||||
lines.push(`add ${parts.join(" ")}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (lines.length <= 3) {
|
||||
throw new Error("RouterOS вернул пустой export и fallback-данные недоступны")
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error type ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user