fix(ui): показывать достоверный снимок контейнера

This commit is contained in:
Denozordec committed 2026-10-01 11:02:31 +07:00
1 parent c419a46955
commit 3ceb8c038c
14 files changed
+117 -141

No files matched your search

@@ -4,7 +4,7 @@ import { MikrotikError } from "./mikrotik.js"
import { containersStateSchema } from "@mmapp/contracts/containers"
const server = { id: 1, name: "Router" } as Parameters<typeof readContainerServerState>[0]
const row = { ".id": "*A", name: "nginx", "remote-image": "nginx:alpine", interface: "veth1", envlist: "e", mounts: "m", status: "running", "start-on-boot": "yes", cpu: "0", "memory-high": "0" }
const row = { ".id": "*A", name: "nginx", "remote-image": "nginx:alpine", interface: "veth1", envlist: "e", mounts: "m", status: "running", "start-on-boot": "yes", cpu: "0", "memory-current": "0", "memory-high": "52428800" }
const read = async (path: string): Promise<unknown> => path === "/container" ? [row] : path === "/container/envs" ? [{name:"e",key:"A",value:"B"}] : [{name:"m",src:"/disk",dst:"/data"}]
const good = await readContainerServerState(server, read)
assert.equal(good.availability, "available"); assert.equal(good.containers[0].status,"running")
@@ -32,3 +32,6 @@ for (const value of [
{...aggregate,servers:[{...unsupported,failures:[{resource:"envs",error:"x"}]}]},
]) assert.equal(containersStateSchema.safeParse(value).success,false)
console.log("Containers state: independent devices, malformed/duplicate IDs, unknown status/boot, known empty, missing package, partial dependencies, zeros and envelope validation; no DB/RouterOS")
const limitOnly=await readContainerServerState(server,async path=>path==="/container"?[{".id":"*C","memory-high":"52428800"}]:[]);assert.equal(limitOnly.containers[0].memMb,undefined)
const modern=await readContainerServerState(server,async path=>path==="/container"?[{...row,envlist:"e,e2",mounts:undefined,mountlists:"m"}]:path==="/container/envs"?[{list:"e",key:"A",value:"1"},{list:"e2",key:"B",value:"2"}]:[{list:"m",src:"/disk",dst:"/data"}]);assert.equal(modern.containers[0].envs.length,2);assert.equal(modern.containers[0].mounts.length,1)
+10 -6
View File
@@ -7,14 +7,14 @@ type ServerRow = Parameters<typeof mapContainerRow>[0]
type Reader = (path: string) => Promise<unknown>
const rawSchema = z.array(z.object({
".id": z.string().min(1), name: z.string().optional(), tag: z.string().optional(), "remote-image": z.string().optional(),
interface: z.string().optional(), envlist: z.string().optional(), mounts: z.string().optional(),
interface: z.string().optional(), envlist: z.string().optional(), mounts: z.string().optional(), mountlists: z.string().optional(),
cmd: z.string().optional(), "start-on-boot": z.string().optional(), comment: z.string().optional(),
status: z.string().optional(), "memory-high": z.string().optional(), cpu: z.string().optional(),
status: z.string().optional(), "memory-high": z.string().optional(), "memory-current": z.string().optional(), cpu: z.string().optional(),
})).superRefine((rows, ctx) => {
if (new Set(rows.map(r => r[".id"])).size !== rows.length) ctx.addIssue({ code: "custom", message: "Дубли RouterOS ID" })
})
const envSchema = z.array(z.object({ name: z.string().optional(), key: z.string().optional(), value: z.string().optional() }))
const mountSchema = z.array(z.object({ name: z.string().optional(), src: z.string().optional(), dst: z.string().optional() }))
const envSchema = z.array(z.object({ name: z.string().optional(), list: z.string().optional(), key: z.string().min(1), value: z.string() }).refine(row => !!(row.list || row.name), "Отсутствует список env"))
const mountSchema = z.array(z.object({ name: z.string().optional(), list: z.string().optional(), src: z.string().optional(), dst: z.string().min(1) }).refine(row => !!(row.list || row.name), "Отсутствует список mounts"))
const errorMessage = (error: unknown) => error instanceof z.ZodError ? "Некорректный ответ RouterOS" : (error instanceof Error ? error.message.slice(0, 500) : "Ошибка RouterOS")
function missingPackage(error: unknown) {
return error instanceof MikrotikError && (error.statusCode === 404 || /no such command|unknown command/i.test(error.body))
@@ -36,10 +36,14 @@ export async function readContainerServerState(server: ServerRow, read: Reader):
if (envs.status === "rejected") failures.push({ resource: "envs", error: errorMessage(envs.reason) })
if (mounts.status === "rejected") failures.push({ resource: "mounts", error: errorMessage(mounts.reason) })
const containers = raw.map((row, index) => {
const mapped = mapContainerRow(server, row, envs.status === "fulfilled" ? envs.value : [], mounts.status === "fulfilled" ? mounts.value : [], index)
const envRows = envs.status === "fulfilled" ? envs.value : []
const mountRows = mounts.status === "fulfilled" ? mounts.value : []
const mapped = mapContainerRow(server, { ...row, mounts: row.mountlists ?? row.mounts }, envRows.map(item => ({ ...item, name: item.list ?? item.name })), mountRows.map(item => ({ ...item, name: item.list ?? item.name })), index)
const envLists = new Set((row.envlist ?? "").split(",").map(name => name.trim()).filter(Boolean))
mapped.envs = envRows.filter(item => envLists.has(item.list ?? item.name ?? "")).map(item => ({ key: item.key, value: item.value }))
const status = (row.status ?? "").toLowerCase()
const boot = row["start-on-boot"]
const mem = Number.parseInt(row["memory-high"] ?? "", 10)
const mem = Number(row["memory-current"]?.trim() || NaN)
return { ...mapped, image: row["remote-image"] ? mapped.image : "", tag: row["remote-image"] ? mapped.tag : "",
status: status === "running" || status === "stopped" ? status : status === "error" || status === "failed" ? "error" : "unknown",
startOnBoot: boot === "true" || boot === "yes" ? true : boot === "false" || boot === "no" ? false : null,