fix(containers): исключить опрос выключенных серверов и ложные пустые данные
This commit is contained in:
1 parent
74151176d7
commit
dc7b22940a
16 files changed
+468
-86
No files matched your search
@@ -36,7 +36,7 @@
|
||||
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||
"test:geoip": "tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts",
|
||||
"test:containers": "tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
|
||||
"test:containers": "tsx src/services/containers-live.test.ts && tsx src/services/containers-state.test.ts && tsx src/services/container-removal.test.ts && tsx src/routes/containers-state.test.ts",
|
||||
"test:certificates": "tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts",
|
||||
"test:system-database": "tsx src/services/system-database-partition-restore.test.ts && tsx src/services/system-database-cache.test.ts && tsx src/services/database-background-work.test.ts && tsx src/services/database-request-gate.test.ts && tsx src/services/traffic-flow-worker-shutdown.test.ts && tsx src/services/system-database-restore-execution.test.ts && tsx src/services/system-database-operation.test.ts && tsx src/routes/system-database.test.ts && tsx src/plugins/auth.system-database.test.ts",
|
||||
"test:all": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:ipsec && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database && npm run test:server-privacy",
|
||||
|
||||
@@ -17,14 +17,21 @@ failed=true;assert.equal((await read()).statusCode,500)
|
||||
assert.equal(permissionForRequest("GET","/api/containers/state"),"mm:dashboard:read")
|
||||
assert.equal(permissionForRequest("POST","/api/servers/1/containers/start"),"mm:servers:write")
|
||||
await app.close();console.log("Containers HTTP: explicit empty/partial error, invalid state 502, DB failure 500, existing permission contract")
|
||||
let enabled = true, missing = false, calls = 0, removalError: Error | undefined
|
||||
let enabled = true, missing = false, calls = 0, reads = 0, failRead = false, removalError: Error | undefined
|
||||
const readError = new Error("RouterOS refused")
|
||||
const mutations = Fastify();mutations.setValidatorCompiler(validatorCompiler);mutations.setSerializerCompiler(serializerCompiler)
|
||||
await mutations.register(createContainersRoutes({getServer:async()=>missing?null:({id:1,enabled} as NonNullable<Awaited<ReturnType<NonNullable<Parameters<typeof createContainersRoutes>[0]>["getServer"]>>>),remove:async()=>{calls++;if(removalError)throw removalError}}),{prefix:"/api"})
|
||||
await mutations.register(createContainersRoutes({getServer:async()=>missing?null:({id:1,enabled} as NonNullable<Awaited<ReturnType<NonNullable<Parameters<typeof createContainersRoutes>[0]>["getServer"]>>>),getLegacyCatalog:async()=>{if(failRead)throw readError;return []},getServerContainers:async()=>{reads++;if(failRead)throw readError;return []},remove:async()=>{calls++;if(removalError)throw removalError}}),{prefix:"/api"})
|
||||
const remove = ()=>mutations.inject({method:"POST",url:"/api/servers/1/containers/remove",payload:{rosId:"*A"}})
|
||||
enabled=false;assert.equal((await remove()).statusCode,409);assert.equal(calls,0)
|
||||
missing=true;assert.equal((await remove()).statusCode,404);assert.equal(calls,0)
|
||||
missing=false;enabled=true;removalError=new ContainerRemovalConflict("Остановите контейнер");assert.equal((await remove()).statusCode,409)
|
||||
removalError=new Error("read failed");assert.equal((await remove()).statusCode,502)
|
||||
removalError=undefined;assert.deepEqual((await remove()).json(),{ok:true})
|
||||
const readSelected=()=>mutations.inject({method:"GET",url:"/api/servers/1/containers"})
|
||||
enabled=false;assert.equal((await readSelected()).statusCode,409);assert.equal(reads,0)
|
||||
missing=true;assert.equal((await readSelected()).statusCode,404);assert.equal(reads,0)
|
||||
missing=false;enabled=true;assert.equal((await readSelected()).statusCode,200);assert.equal(reads,1)
|
||||
failRead=true;assert.equal((await readSelected()).statusCode,502)
|
||||
assert.equal((await mutations.inject({method:"GET",url:"/api/containers"})).statusCode,502)
|
||||
assert.equal(permissionForRequest("POST","/api/servers/1/containers/remove"),"mm:servers:write")
|
||||
await mutations.close();console.log("Containers mutations HTTP: disabled/missing server cannot dispatch; removal conflict 409, uncertainty 502, confirmed success and write permission")
|
||||
@@ -21,7 +21,7 @@ const RosIdBodySchema = z.object({
|
||||
type RosIdBody = z.infer<typeof RosIdBodySchema>
|
||||
type MutateFn = typeof startContainer
|
||||
|
||||
export function createContainersRoutes(deps: { getState?: typeof listContainersState; getServer?: typeof getEnabledServerById; remove?: MutateFn } = {}): FastifyPluginAsyncZod {
|
||||
export function createContainersRoutes(deps: { getState?: typeof listContainersState; getServer?: typeof getEnabledServerById; getLegacyCatalog?: typeof listContainers; getServerContainers?: typeof listContainersForServer; remove?: MutateFn } = {}): FastifyPluginAsyncZod {
|
||||
return async (app) => {
|
||||
app.get("/containers/state", async (_req, reply) => {
|
||||
const state = containersStateSchema.safeParse(await (deps.getState ?? listContainersState)())
|
||||
@@ -29,16 +29,25 @@ export function createContainersRoutes(deps: { getState?: typeof listContainersS
|
||||
return reply.send(state.data)
|
||||
})
|
||||
app.get("/containers", async (_req, reply) => {
|
||||
const containers = await listContainers()
|
||||
try {
|
||||
const containers = await (deps.getLegacyCatalog ?? listContainers)()
|
||||
return reply.send({ containers })
|
||||
} catch (error) {
|
||||
return reply.status(502).send({ error: error instanceof Error ? error.message : "Не удалось получить контейнеры" })
|
||||
}
|
||||
})
|
||||
|
||||
app.get("/servers/:id/containers", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = await getEnabledServerById(params.id)
|
||||
const server = await (deps.getServer ?? getEnabledServerById)(params.id)
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
const containers = await listContainersForServer(server)
|
||||
if (!server.enabled) return reply.status(409).send({ error: "Сервер выключен. Опрос контейнеров недоступен." })
|
||||
try {
|
||||
const containers = await (deps.getServerContainers ?? listContainersForServer)(server)
|
||||
return reply.send({ containers })
|
||||
} catch (error) {
|
||||
return reply.status(502).send({ error: error instanceof Error ? error.message : "Не удалось получить контейнеры" })
|
||||
}
|
||||
})
|
||||
|
||||
function registerMutate(path: string, fn: MutateFn) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { mapContainerRow } from "./containers-live.js"
|
||||
import { mapContainerRow, listContainersForServer, getEnabledServerById } from "./containers-live.js"
|
||||
import { createServer } from "node:http"
|
||||
|
||||
const server = {
|
||||
id: 3,
|
||||
@@ -44,3 +45,31 @@ assert.deepEqual(row.mounts, [
|
||||
assert.equal(row.startOnBoot, true)
|
||||
|
||||
console.log("containers-live.test.ts: ok")
|
||||
for(const [native,expected] of [[{running:"true"},"running"],[{stopped:"true"},"stopped"],[{},"unknown"],[{running:true,stopped:true},"unknown"]] as const){
|
||||
const result=mapContainerRow(server,{".id":"*B",...native},[],[],0)
|
||||
assert.equal(result.status,expected);assert.equal(result.startOnBoot,null);assert.equal(result.image,"");assert.equal(result.tag,"")
|
||||
}
|
||||
const modern=mapContainerRow(server,{".id":"*C",envlists:"first,second",mountlists:"config","memory-current":"0","memory-high":"52428800",cpu:"0"},[{list:"first",key:"A",value:"1"},{list:"second",key:"B",value:"2"}],[{list:"config",dst:"/etc"}],0)
|
||||
assert.equal(modern.envs.length,2);assert.equal(modern.mounts.length,1);assert.equal(modern.memMb,0);assert.equal(modern.cpu,0)
|
||||
const limitOnly=mapContainerRow(server,{".id":"*D","memory-high":"52428800",cpu:"5bad"},[],[],0)
|
||||
assert.equal(limitOnly.memMb,undefined);assert.equal(limitOnly.cpu,undefined)
|
||||
for(const invalid of ["1abc","","0","-1","1.5","9007199254740993"])assert.equal(await getEnabledServerById(invalid),null)
|
||||
let mode="ok",requests=0
|
||||
const router=createServer((req,res)=>{
|
||||
requests++;res.setHeader("Content-Type","application/json")
|
||||
if(mode==="unauthorized"){res.writeHead(401);res.end(JSON.stringify({error:401,message:"Unauthorized"}));return}
|
||||
if(mode==="unsupported"){res.writeHead(404);res.end(JSON.stringify({error:404,message:"no such command"}));return}
|
||||
if(mode==="malformed"){res.end(JSON.stringify({containers:[]}));return}
|
||||
if(mode==="dependency"&&req.url==="/rest/container/envs"){res.writeHead(500);res.end(JSON.stringify({error:500}));return}
|
||||
res.end(JSON.stringify(req.url==="/rest/container"?[{".id":"*A",name:"native",running:"true"}]:[]))
|
||||
})
|
||||
await new Promise<void>(resolve=>router.listen(0,"127.0.0.1",resolve))
|
||||
const address=router.address();assert(address&&typeof address!=="string")
|
||||
const device={...server,host:"127.0.0.1",port:address.port,useSsl:false,verifySsl:false,enabled:true,username:"fixture",password:"fixture"} as Parameters<typeof listContainersForServer>[0]
|
||||
try{
|
||||
const before=requests;await assert.rejects(listContainersForServer({...device,enabled:false}),/выключен/);assert.equal(requests,before)
|
||||
const rows=await listContainersForServer(device);assert.equal(rows[0].status,"running");assert.equal(rows[0].startOnBoot,null)
|
||||
for(const failure of ["unauthorized","malformed","dependency"]){mode=failure;await assert.rejects(listContainersForServer(device),/Не удалось полностью/)}
|
||||
mode="unsupported";assert.deepEqual(await listContainersForServer(device),[])
|
||||
}finally{await new Promise<void>((resolve,reject)=>router.close(error=>error?reject(error):resolve()))}
|
||||
console.log("Legacy containers: real loopback HTTP handles native flags, auth/malformed/partial errors; disabled server makes zero requests; invalid IDs never query DB")
|
||||
@@ -2,7 +2,9 @@ import {boundedCompleteCount,completeContainerCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient, MikrotikError } from "./mikrotik.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { containerRuntimeStatus } from "./container-runtime-status.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
@@ -13,28 +15,41 @@ interface RosContainer {
|
||||
"remote-image"?: string
|
||||
interface?: string
|
||||
envlist?: string
|
||||
envlists?: string
|
||||
mounts?: string
|
||||
mountlists?: string
|
||||
cmd?: string
|
||||
"start-on-boot"?: string
|
||||
comment?: string
|
||||
status?: string
|
||||
"memory-high"?: string
|
||||
"memory-current"?: string
|
||||
cpu?: string
|
||||
running?: string | boolean
|
||||
stopped?: string | boolean
|
||||
error?: string | boolean
|
||||
failed?: string | boolean
|
||||
extracting?: string | boolean
|
||||
downloading?: string | boolean
|
||||
starting?: string | boolean
|
||||
stopping?: string | boolean
|
||||
}
|
||||
|
||||
interface RosContainerEnv {
|
||||
name?: string
|
||||
list?: string
|
||||
key?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface RosContainerMount {
|
||||
name?: string
|
||||
list?: string
|
||||
src?: string
|
||||
dst?: string
|
||||
}
|
||||
|
||||
export type ContainerLiveStatus = "running" | "stopped" | "error"
|
||||
export type ContainerLiveStatus = "running" | "stopped" | "error" | "unknown"
|
||||
|
||||
export type ContainerLive = {
|
||||
id: string
|
||||
@@ -48,24 +63,13 @@ export type ContainerLive = {
|
||||
mounts: { dst: string; src?: string }[]
|
||||
interfaces: string[]
|
||||
cmd?: string
|
||||
startOnBoot: boolean
|
||||
startOnBoot: boolean | null
|
||||
comment: string
|
||||
uptime?: string
|
||||
cpu?: number
|
||||
memMb?: number
|
||||
}
|
||||
|
||||
function rosYes(v: string | undefined): boolean {
|
||||
return v === "true" || v === "yes"
|
||||
}
|
||||
|
||||
function mapStatus(raw: string | undefined): ContainerLiveStatus {
|
||||
const s = (raw ?? "").toLowerCase()
|
||||
if (s === "running") return "running"
|
||||
if (s === "error" || s === "failed") return "error"
|
||||
return "stopped"
|
||||
}
|
||||
|
||||
function splitCsv(v: string | undefined): string[] {
|
||||
return (v ?? "")
|
||||
.split(",")
|
||||
@@ -82,17 +86,7 @@ function parseImageTag(c: RosContainer): { image: string; tag: string } {
|
||||
}
|
||||
return { image: remote, tag: (c.tag ?? "latest").trim() || "latest" }
|
||||
}
|
||||
return { image: (c.name ?? "").trim(), tag: (c.tag ?? "latest").trim() || "latest" }
|
||||
}
|
||||
|
||||
function isMissingPackage(err: unknown): boolean {
|
||||
if (err instanceof MikrotikError) {
|
||||
if (err.statusCode === 404) return true
|
||||
const body = err.body.toLowerCase()
|
||||
return body.includes("no such command") || body.includes("not found") || body.includes("unknown")
|
||||
}
|
||||
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase()
|
||||
return msg.includes("no such command") || msg.includes("404")
|
||||
return { image: "", tag: "" }
|
||||
}
|
||||
|
||||
export function mapContainerRow(
|
||||
@@ -105,14 +99,13 @@ export function mapContainerRow(
|
||||
const rosId = String(c[".id"] ?? `c-${idx}`)
|
||||
const name = (c.name ?? "").trim() || `container-${idx + 1}`
|
||||
const { image, tag } = parseImageTag(c)
|
||||
const envlist = (c.envlist ?? "").trim()
|
||||
const mountNames = new Set(splitCsv(c.mounts))
|
||||
const envRows = envlist
|
||||
? envs.filter((e) => (e.name ?? "").trim() === envlist && (e.key ?? "").trim())
|
||||
: []
|
||||
const mountRows = mounts.filter((m) => mountNames.has((m.name ?? "").trim()) && (m.dst ?? "").trim())
|
||||
const cpuRaw = Number.parseInt(c.cpu ?? "", 10)
|
||||
const memRaw = Number.parseInt(c["memory-high"] ?? "", 10)
|
||||
const envNames = new Set(splitCsv(c.envlists ?? c.envlist))
|
||||
const mountNames = new Set(splitCsv(c.mountlists ?? c.mounts))
|
||||
const envRows = envs.filter(e => envNames.has((e.list ?? e.name ?? "").trim()) && (e.key ?? "").trim())
|
||||
const mountRows = mounts.filter((m) => mountNames.has((m.list ?? m.name ?? "").trim()) && (m.dst ?? "").trim())
|
||||
const cpuRaw = Number(c.cpu?.trim() || NaN)
|
||||
const memRaw = Number(c["memory-current"]?.trim() || NaN)
|
||||
const boot = c["start-on-boot"]
|
||||
return {
|
||||
id: `${server.id}-${rosId}`,
|
||||
rosId,
|
||||
@@ -120,56 +113,35 @@ export function mapContainerRow(
|
||||
serverId: String(server.id),
|
||||
image,
|
||||
tag,
|
||||
status: mapStatus(c.status),
|
||||
status: containerRuntimeStatus({ ...c }),
|
||||
envs: envRows.map((e) => ({ key: e.key ?? "", value: e.value ?? "" })),
|
||||
mounts: mountRows.map((m) => ({ dst: m.dst ?? "", src: m.src || undefined })),
|
||||
interfaces: splitCsv(c.interface),
|
||||
cmd: (c.cmd ?? "").trim() || undefined,
|
||||
startOnBoot: rosYes(c["start-on-boot"]),
|
||||
startOnBoot: boot === "true" || boot === "yes" ? true : boot === "false" || boot === "no" ? false : null,
|
||||
comment: c.comment ?? "",
|
||||
cpu: Number.isFinite(cpuRaw) ? cpuRaw : undefined,
|
||||
memMb: Number.isFinite(memRaw) ? Math.round(memRaw / (1024 * 1024)) || undefined : undefined,
|
||||
cpu: Number.isFinite(cpuRaw) && cpuRaw >= 0 && cpuRaw <= 100 ? cpuRaw : undefined,
|
||||
memMb: Number.isFinite(memRaw) && memRaw >= 0 ? Math.round(memRaw / (1024 * 1024)) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContainersForServer(server: ServerRow): Promise<ContainerLive[]> {
|
||||
if (!server.enabled) throw new Error("Сервер выключен. Опрос контейнеров недоступен.")
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const [raw, envsRaw, mountsRaw] = await Promise.all([
|
||||
client.get<RosContainer[]>("/container"),
|
||||
client.get<RosContainerEnv[]>("/container/envs").catch(() => [] as RosContainerEnv[]),
|
||||
client.get<RosContainerMount[]>("/container/mounts").catch(() => [] as RosContainerMount[]),
|
||||
])
|
||||
const list = Array.isArray(raw) ? raw : []
|
||||
const envs = Array.isArray(envsRaw) ? envsRaw : []
|
||||
const mounts = Array.isArray(mountsRaw) ? mountsRaw : []
|
||||
return list.map((c, idx) => mapContainerRow(server, c, envs, mounts, idx))
|
||||
} catch (err) {
|
||||
if (isMissingPackage(err)) return []
|
||||
throw err
|
||||
}
|
||||
const { readContainerServerState } = await import("./containers-state.js")
|
||||
const state = await readContainerServerState(server, path => client.get(path))
|
||||
if (state.availability === "error" || state.failures.length) throw new Error("Не удалось полностью получить контейнеры сервера " + server.name + ": " + state.failures.map(f => f.error).join("; "))
|
||||
return state.containers
|
||||
}
|
||||
|
||||
export async function listContainers(): Promise<ContainerLive[]> {
|
||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
const results = await Promise.all(
|
||||
enabledServers.map(async (server) => {
|
||||
try {
|
||||
return await fetchContainersForServer(server)
|
||||
} catch {
|
||||
return [] as ContainerLive[]
|
||||
}
|
||||
}),
|
||||
)
|
||||
const results = await Promise.all(enabledServers.map(fetchContainersForServer))
|
||||
return results.flat()
|
||||
}
|
||||
|
||||
export async function listContainersForServer(server: ServerRow): Promise<ContainerLive[]> {
|
||||
try {
|
||||
return await fetchContainersForServer(server)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return fetchContainersForServer(server)
|
||||
}
|
||||
|
||||
export async function countContainers():Promise<number|undefined>{return boundedCompleteCount(()=>listContainersState(),completeContainerCount)}
|
||||
@@ -196,8 +168,8 @@ export async function removeContainer(server: ServerRow, rosId: string): Promise
|
||||
}
|
||||
|
||||
export async function getEnabledServerById(serverId: string | number) {
|
||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||
if (!Number.isFinite(id)) return null
|
||||
const id = parseDbServerId(serverId)
|
||||
if (id === null) return null
|
||||
return (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0] ?? null
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readContainerServerState, collectContainersState } from "./containers-s
|
||||
import { MikrotikError } from "./mikrotik.js"
|
||||
import { containersStateSchema } from "@mmapp/contracts/containers"
|
||||
|
||||
const server = { id: 1, name: "Router" } as Parameters<typeof readContainerServerState>[0]
|
||||
const server = { id: 1, name: "Router", enabled: true } 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-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)
|
||||
@@ -49,3 +49,8 @@ const modern=await readContainerServerState(server,async path=>path==="/containe
|
||||
|
||||
let factories=0
|
||||
const setupFailure=await collectContainersState([server,{...server,id:2}],srv=>{if(srv.id===2)throw new Error("client setup refused");factories++;return read});assert.equal(setupFailure.servers[0].containers.length,1);assert.equal(setupFailure.servers[1].availability,"error");assert.equal(setupFailure.servers[1].failures[0].error,"client setup refused");assert.equal(factories,1)
|
||||
let disabledReads=0
|
||||
const disabled=await readContainerServerState({...server,enabled:false},async()=>{disabledReads++;throw new Error("Must never poll disabled")})
|
||||
assert.equal(disabledReads,0);assert.equal(disabled.availability,"error");assert.match(disabled.failures[0].error,/выключен/)
|
||||
const enabledOnly=await collectContainersState([server,{...server,id:2,enabled:false}],srv=>{assert.equal(srv.id,1);return read})
|
||||
assert.deepEqual(enabledOnly.servers.map(s=>s.serverId),[1])
|
||||
@@ -26,6 +26,7 @@ function missingPackage(error: unknown) {
|
||||
}
|
||||
export async function readContainerServerState(server: ServerRow, read: Reader): Promise<ContainerServerState> {
|
||||
const base = { serverId: server.id, collectedAt: new Date().toISOString(), containers: [], failures: [] }
|
||||
if (!server.enabled) return containerServerStateSchema.parse({ ...base, availability: "error", failures: [{ resource: "containers", error: "Сервер выключен. Опрос контейнеров недоступен." }] })
|
||||
let raw: z.infer<typeof rawSchema>
|
||||
try { raw = rawSchema.parse(await read("/container")) }
|
||||
catch (error) {
|
||||
@@ -57,7 +58,7 @@ export async function readContainerServerState(server: ServerRow, read: Reader):
|
||||
return containerServerStateSchema.parse({ serverId: server.id, collectedAt, availability: "available", containers, failures })
|
||||
}
|
||||
export async function collectContainersState(servers: ServerRow[], makeReader: (server: ServerRow) => Reader): Promise<ContainersState> {
|
||||
const states = await Promise.all(servers.map(server => {
|
||||
const states = await Promise.all(servers.filter(server => server.enabled).map(server => {
|
||||
let reader: Reader | undefined
|
||||
return readContainerServerState(server, path => { reader ??= makeReader(server); return reader(path) })
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
|
||||
|
||||
@@ -18,6 +18,20 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro
|
||||
|
||||
BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json).
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
В 247 UI отправлял DELETE работающего контейнера и получал 502. Теперь dialog использует последнее состояние выбранного serverId/rosId: удалить можно только stopped; для running/unknown/error показана инструкция остановки и обновления. Backend независимо читает native список и проверяет однозначное stopped, не останавливает контейнер скрыто. Отсутствующий target или неподходящее состояние дают 409 без DELETE; после DELETE каталог читается снова, сохранённый target/сбой/некорректный ответ не становятся ok=true. Выключенный сервер отклоняется до команды. Каталоги/файлы не удаляются отдельными командами приложения; поведение native remove не подменяется обещанием очистки.
|
||||
|
||||
+15
-1
@@ -1,9 +1,23 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-14. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
В 247 UI отправлял DELETE работающего контейнера и получал 502. Теперь dialog использует последнее состояние выбранного serverId/rosId: удалить можно только stopped; для running/unknown/error показана инструкция остановки и обновления. Backend независимо читает native список и проверяет однозначное stopped, не останавливает контейнер скрыто. Отсутствующий target или неподходящее состояние дают 409 без DELETE; после DELETE каталог читается снова, сохранённый target/сбой/некорректный ответ не становятся ok=true. Выключенный сервер отклоняется до команды. Каталоги/файлы не удаляются отдельными командами приложения; поведение native remove не подменяется обещанием очистки.
|
||||
|
||||
+15
-1
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -38,6 +38,20 @@
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка12.3.83 в границах журнала. Внешняя12.5: блоки1–236 завершены в записанных scopes, этап открыт. Runtime backend ux-backup234/frontend ux-routing218; backend25/UI52 и дополнительные SQL/UDP222 прошли. |
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
В 247 UI отправлял DELETE работающего контейнера и получал 502. Теперь dialog использует последнее состояние выбранного serverId/rosId: удалить можно только stopped; для running/unknown/error показана инструкция остановки и обновления. Backend независимо читает native список и проверяет однозначное stopped, не останавливает контейнер скрыто. Отсутствующий target или неподходящее состояние дают 409 без DELETE; после DELETE каталог читается снова, сохранённый target/сбой/некорректный ответ не становятся ok=true. Выключенный сервер отклоняется до команды. Каталоги/файлы не удаляются отдельными командами приложения; поведение native remove не подменяется обещанием очистки.
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-14
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
Это актуальная матрица после 12.5.248. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.249. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend/frontend `ux-containers248`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-containers249`, frontend `ux-containers248`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
@@ -196,7 +210,7 @@ Portal offline JWT действует доexp после logout/role change; э
|
||||
|
||||
## Следующая последовательность — 2026-10-14
|
||||
|
||||
1. **Контейнеры249:** lifecycle248 принят; проверить legacy чтение выключенного сервера и native статусы старого DTO. Package и собственные объекты очищены. CI245 принят; ACME и remote Gitea publishing требуют своих доступов/разрешённого scope.
|
||||
1. **ACME250:** стенд pngs.top доступен; разрешён mmt* scope, token/zone verified read-only. Проверить реальную выдачу/импорт/результат и очистку. Containers248–249 приняты; общая backend-регрессия249 passed с тремя записанными SQL skips. Remote Gitea publishing требует своего разрешённого scope.
|
||||
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
|
||||
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
|
||||
|
||||
|
||||
+15
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -4152,6 +4152,20 @@ Real Portal/browser API, provider local verified: создан один job ро
|
||||
Runtime backend ux-backup234/frontend ux-routing218 healthy; новое application image для volume-only fix не требовалось. Пароли CHR не менялись, S3 mutations0 в235, configs body/JWT не опубликованы, private local manifest удалён. Исходные10 missing local files не восстановлены и records не удалены. Force-recreate — не host reboot/power loss/fsync/DR приёмка. Далее236 — UX недоступных local files, затем version retention границы и оставшиеся7 группы матрицы; multi-instance guard не принимается. Этап12.5 открыт.
|
||||
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
В 247 UI отправлял DELETE работающего контейнера и получал 502. Теперь dialog использует последнее состояние выбранного serverId/rosId: удалить можно только stopped; для running/unknown/error показана инструкция остановки и обновления. Backend независимо читает native список и проверяет однозначное stopped, не останавливает контейнер скрыто. Отсутствующий target или неподходящее состояние дают 409 без DELETE; после DELETE каталог читается снова, сохранённый target/сбой/некорректный ответ не становятся ok=true. Выключенный сервер отклоняется до команды. Каталоги/файлы не удаляются отдельными командами приложения; поведение native remove не подменяется обещанием очистки.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Актуальное продолжение 2026-10-14: 248 — удаление контейнера исправлено и принято на реальном CHR: UI требует остановку, API независимо проверяет её, DELETE подтверждается повторным чтением. Running/unknown/error заблокированы; повтор отсутствующего target даёт 409. 29 E2E, backend-профиль, build и ESLint passed. Backend/frontend ux-containers248 развёрнуты; собственные объекты и пакет удалены двумя разрешёнными reboot, исходный export/packages совпали. Прежний пароль подтвердился SFTP и прямым REST200. Следующий 249 — аудит legacy чтения контейнеров и запрет опроса выключенного сервера. Этап 12.5 открыт.
|
||||
Актуальное продолжение 2026-10-14: 249 — legacy GET контейнеров не опрашивают выключенный сервер и не превращают ошибки в пустой список. Единый mapper сохраняет unknown, native flags/envlists/mountlists и фактическую память. Backend ux-containers249/frontend ux-containers248; 25 backend-профилей passed за378124ms, три специализированные SQL-ветки явно skipped. Owned БД/роль/container удалены. Real Portal/read UI1440/390 passed, writes0. Следующий250 — ACME на собственном mmt* поддомене pngs.top; предоставленный токен проверен read-only, выпуск ещё не выполнен. Этап12.5 открыт.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -34,6 +34,20 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro
|
||||
|
||||
BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json).
|
||||
|
||||
## 12.5.249 — достоверное legacy чтение контейнеров и общая backend-регрессия (2026-10-14)
|
||||
|
||||
Аудит выявил: GET выбранного сервера обходил enabled gate; legacy mapper считал отсутствие status остановкой, memory-high — фактической памятью, скрывал ошибки загрузки пустым массивом. Прямой GET теперь даёт409 для выключенного и404 для отсутствующего сервера до dispatch; прямой service/read тоже не создаёт RouterOS клиент для disabled, aggregate исключает disabled до reader factory. Ошибки/частичные ответы legacy GET дают502; unsupported сохраняет прежний legacy [] для совместимости, подробное availability предоставляется /api/containers/state. ID разбирается общим строгим parseDbServerId, без принятия префикса1abc.
|
||||
|
||||
Legacy service переиспользует канонический readContainerServerState вместо второго скрывающего ошибки опроса. Общий mapper учитывает running/stopped/error flags и unknown, envlists/mountlists и современные list aliases, nullable startOnBoot, memory-current с известным нулём; memory-high и имя контейнера не подменяют расход памяти и неизвестный образ. Десятичный/неверный CPU не обрезается parseInt. Изменение legacy status включает unknown; потребители нового UI уже используют этот статус. В репозитории frontend не обращается к старым GET.
|
||||
|
||||
test:containers дополнен legacy service profile и включён в общий test:all/CI. Настоящий loopback HTTP-транспорт без DB: valid/native running;401/malformed/dependency error не становятся empty; unsupported сохраняет []. Disabled makes0 requests; invalid ID returnsnull до DB. HTTP проверяет selected disabled/missing без read dispatch и502 вместо fake success. Direct state иaggregate disabled guards проверены отдельно. Первый sandbox запуск loopback получил EACCES и не принят; elevated профиль прошёл. Contracts/backend build и ESLint passed; Docker Node22 профиль тоже exit0.
|
||||
|
||||
Из-за изменения общей основы выполнены все25 профилей test:all на собственной ограниченной PostgreSQL БД/роли, без production DSN. Все exit0; сумма378124ms (6мин18с), это не число индивидуальных assertions. Три специальные SQL-ветки пропущены из-за отдельных env/namespace: Monthly149, IPv6 UDP/worker144, network-disabled-server152 (MMAPP_OWN_TEST_DB). Это известные явные ограничения текущего объединённого запуска, не приёмка этих веток. Остальные conditional skip строки сохраняются числом в протоколе, не превращаются в live доказательства. Собственная БД/роль/контейнер удалены, отсутствие проверено. [Регрессия](ux-ui/acceptance/containers249-regression.json).
|
||||
|
||||
Backend ux-containers249 собран/развёрнут и healthy; frontend ux-containers248 сохранён, PostgreSQL/Portal не пересоздавались. Backend source524/compiled1080 совпали без missing/different; версия1.0.0-test.backup249 — метка лабораторной сборки, неGitSHA. [Deployment](ux-ui/acceptance/containers249-deployment.json). Настоящий Portal/read UI1440/390: typed state200 показывает оба CHR unsupported после удаления пакета248, legacy global/selected GET200[] совместимы, overflow/JS errors0, non-auth API writes0. Никаких новых native объектов/перезагрузок/изменений паролей. [Live UI](ux-ui/acceptance/containers249-live-ui.json). ReUI audit checklist для alert-dialog прочитан: используются существующие props/copy; vendored компоненты/стили/тайминги не менялись.
|
||||
|
||||
Пользователь предоставил стенд ACME: pngs.top и разрешённые поддомены с префиксом mmt*. Read-only Cloudflare token verify и exact zone query вернули200/success; token/zone active, account matched. Токен/почта/account ID сохранены только в ignored private fixture, в Git не добавляются. Следующий250 — issuance/импорт/результат через реальный API/UI с очисткой собственных DNS/certificate/file объектов и возвратом исходных настроек. Пока реальная выдача не выполнена. Обнаружено, что текущий issue автоматически назначает сертификат www-ssl/api-ssl; перед применением подготовлен private baseline этих сервисов/сертификатов/файлов/settings. Remote publishing, multi-instance/WAN и прочие ограничения матрицы сохраняются.
|
||||
|
||||
## 12.5.248 — подтверждённое удаление контейнера через UI (2026-10-14)
|
||||
|
||||
В 247 UI отправлял DELETE работающего контейнера и получал 502. Теперь dialog использует последнее состояние выбранного serverId/rosId: удалить можно только stopped; для running/unknown/error показана инструкция остановки и обновления. Backend независимо читает native список и проверяет однозначное stopped, не останавливает контейнер скрыто. Отсутствующий target или неподходящее состояние дают 409 без DELETE; после DELETE каталог читается снова, сохранённый target/сбой/некорректный ответ не становятся ok=true. Выключенный сервер отклоняется до команды. Каталоги/файлы не удаляются отдельными командами приложения; поведение native remove не подменяется обещанием очистки.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"date": "2026-10-14",
|
||||
"block": "12.5.249",
|
||||
"containers": [
|
||||
{
|
||||
"name": "mmapp-test-backend",
|
||||
"image": "mmapp-test/backend:ux-containers249",
|
||||
"imageId": "sha256:b0d1526c683c0ce02dcac89087e13a917e27f743af192d2fda4fe26e4f11f056",
|
||||
"running": true,
|
||||
"health": "healthy"
|
||||
},
|
||||
{
|
||||
"name": "mmapp-test-frontend",
|
||||
"image": "mmapp-test/frontend:ux-containers248",
|
||||
"imageId": "sha256:131d762c00aabeb275b0b17591361a83df2d73a794b27ce06ffcdeaee6424a1e",
|
||||
"running": true,
|
||||
"health": "not-configured"
|
||||
}
|
||||
],
|
||||
"sourceAudit": [
|
||||
{
|
||||
"kind": "backend",
|
||||
"matched": 524
|
||||
}
|
||||
],
|
||||
"compiledAudit": {
|
||||
"matched": 1080,
|
||||
"missing": 0,
|
||||
"different": 0
|
||||
},
|
||||
"frontendChanged": false,
|
||||
"buildExit": 0,
|
||||
"containerProfileOnNode22": 0
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"date": "2026-10-14",
|
||||
"block": "12.5.249",
|
||||
"realPortalLogin": true,
|
||||
"noMocks": true,
|
||||
"stateStatus": 200,
|
||||
"availability": [
|
||||
{
|
||||
"serverId": 1,
|
||||
"availability": "unsupported"
|
||||
},
|
||||
{
|
||||
"serverId": 2,
|
||||
"availability": "unsupported"
|
||||
}
|
||||
],
|
||||
"legacy": [
|
||||
{
|
||||
"path": "/api/containers",
|
||||
"status": 200,
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"path": "/api/servers/1/containers",
|
||||
"status": 200,
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"path": "/api/servers/2/containers",
|
||||
"status": 200,
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"width": 1440,
|
||||
"globalOverflow": false
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"globalOverflow": false
|
||||
}
|
||||
],
|
||||
"pageErrors": [],
|
||||
"nonAuthApiWrites": 0
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"date": "2026-10-14",
|
||||
"block": "12.5.249",
|
||||
"scope": "full backend profile list on owned isolated PostgreSQL; no production DSN",
|
||||
"database": "mm_accept_166_1791980090323",
|
||||
"profiles": [
|
||||
{
|
||||
"profile": "test:probes",
|
||||
"passed": true,
|
||||
"durationMs": 23417,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:exec",
|
||||
"passed": true,
|
||||
"durationMs": 8748,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:alert-engine",
|
||||
"passed": true,
|
||||
"durationMs": 22044,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:alerts-telegram",
|
||||
"passed": true,
|
||||
"durationMs": 15207,
|
||||
"skippedLineCount": 10,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:auth",
|
||||
"passed": true,
|
||||
"durationMs": 11062,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:wireguard",
|
||||
"passed": true,
|
||||
"durationMs": 15491,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:ipsec",
|
||||
"passed": true,
|
||||
"durationMs": 16407,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:traffic-rate",
|
||||
"passed": true,
|
||||
"durationMs": 51136,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:traffic-flow",
|
||||
"passed": true,
|
||||
"durationMs": 55279,
|
||||
"skippedLineCount": 2,
|
||||
"unavailableSqlBranches": [
|
||||
"Monthly SQL: skip (requires an owned isolated database)",
|
||||
"IPv6 UDP/worker/SQL: skip (requires an owned isolated database)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile": "test:users",
|
||||
"passed": true,
|
||||
"durationMs": 7848,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:pg",
|
||||
"passed": true,
|
||||
"durationMs": 10026,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:backups",
|
||||
"passed": true,
|
||||
"durationMs": 12245,
|
||||
"skippedLineCount": 8,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:scheduler",
|
||||
"passed": true,
|
||||
"durationMs": 6936,
|
||||
"skippedLineCount": 5,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:live-maps",
|
||||
"passed": true,
|
||||
"durationMs": 28667,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:ospf-optimizer",
|
||||
"passed": true,
|
||||
"durationMs": 6977,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:recursive-routes",
|
||||
"passed": true,
|
||||
"durationMs": 4117,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:config-sync",
|
||||
"passed": true,
|
||||
"durationMs": 19206,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": [
|
||||
"network-disabled-server.test.ts: invalid IDs passed; own SQL/disabled API checks skipped (MMAPP_OWN_TEST_DB required)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile": "test:ros-file",
|
||||
"passed": true,
|
||||
"durationMs": 1600,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:containers",
|
||||
"passed": true,
|
||||
"durationMs": 8611,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:certificates",
|
||||
"passed": true,
|
||||
"durationMs": 13336,
|
||||
"skippedLineCount": 3,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:netflow-settings",
|
||||
"passed": true,
|
||||
"durationMs": 5398,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:netflow-purge",
|
||||
"passed": true,
|
||||
"durationMs": 2367,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:evolution",
|
||||
"passed": true,
|
||||
"durationMs": 5728,
|
||||
"skippedLineCount": 1,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:system-database",
|
||||
"passed": true,
|
||||
"durationMs": 19615,
|
||||
"skippedLineCount": 9,
|
||||
"unavailableSqlBranches": []
|
||||
},
|
||||
{
|
||||
"profile": "test:server-privacy",
|
||||
"passed": true,
|
||||
"durationMs": 6656,
|
||||
"skippedLineCount": 0,
|
||||
"unavailableSqlBranches": []
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"cleanup": {
|
||||
"databaseRemoved": true,
|
||||
"roleRemoved": true,
|
||||
"containerRemoved": true
|
||||
},
|
||||
"durationMs": 378124
|
||||
}
|
||||
Reference in new issue
Block a user