fix(certificates): подтверждать результат продления и исключить выключенные серверы

This commit is contained in:
Denozordec committed 2026-10-14 21:01:22 +07:00
1 parent 0563dd7cbd
commit 3d23ae4be4
16 files changed
+428 -155

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@
"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-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:certificates": "tsx src/services/certificate-renew-collector.test.ts && 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",
"test:server-privacy": "tsx src/routes/server-secrets-sql.test.ts"
@@ -91,3 +91,9 @@ test("queue handles rejected setup without unhandled rejection", async () => {
await new Promise(resolve => setImmediate(resolve))
assert.equal(f.warnings.length, 1); assert.equal(f.calls(), 0)
})
test("disabled server fails before DNS or device issue", async () => {
const f = fixture(); f.deps.getServer = async () => ({ id: 1, name: "router", enabled: false })
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.calls(), 0); assert.equal(f.row.status, "failed"); assert.match(f.row.error ?? "", /выключен/)
})
@@ -17,7 +17,7 @@ type Dependencies<S> = {
}
/** Separate the network outcome from progress/event persistence failures. */
export function createCertificateIssueRunner<S extends { id: number; name: string }>(deps: Dependencies<S>) {
export function createCertificateIssueRunner<S extends { id: number; name: string; enabled?: boolean }>(deps: Dependencies<S>) {
const runningJobs = new Set<string>()
const report = (error: unknown) => { deps.warn(error) }
const event = async (input: Parameters<typeof appendEvent>[0]) => {
@@ -31,8 +31,8 @@ export function createCertificateIssueRunner<S extends { id: number; name: strin
// A running record from a previous process has an unknown outcome: never replay it.
if (!row || row.status !== "queued") return
const server = await deps.getServer(row.serverId)
if (!server) {
await deps.updateJob(jobId, { status: "failed", step: "failed", finishedAt: new Date().toISOString(), error: "Сервер не найден" })
if (!server || server.enabled === false) {
await deps.updateJob(jobId, { status: "failed", step: "failed", finishedAt: new Date().toISOString(), error: "Сервер не найден или выключен" })
return
}
const domainNames = parseJsonArray(row.domainNames).map(String)
@@ -0,0 +1,62 @@
import assert from "node:assert/strict"
import { createCertificateRenewCollector } from "./certificate-renew-collector.js"
import { getServerRowByIdString } from "./certificates-service.js"
import type { CertificateIssueJobRow } from "../db/schema.js"
import type { RosCertificateRow } from "./certificate-parse.js"
function fixture() {
const settings = { enabled: true, intervalSec: 300, renewBeforeDays: 30, lastCollectedAt: null, lastDurationMs: null, lastError: null, updatedAt: new Date().toISOString() }
const target = { serverId: "1", certName: "cert", domainNames: ["fixture.example.test"], keyType: "rsa2048", trustStore: "www,api", finishedAt: new Date().toISOString() }
let server: Awaited<ReturnType<typeof getServerRowByIdString>> = { id: 1, name: "fixture", enabled: true } as NonNullable<Awaited<ReturnType<typeof getServerRowByIdString>>>
let rows: RosCertificateRow[] = [{ ".id": "*A", name: "cert", flags: "KT", "invalid-before": new Date(Date.now() - 86400000).toISOString(), "invalid-after": new Date(Date.now() + 172800000).toISOString() }]
let job: CertificateIssueJobRow | null = null, outcome: CertificateIssueJobRow["status"] = "done", readFailure = false, active = false
let resultPatch: Partial<CertificateIssueJobRow> = {}, missingResult = false, targetGate: Promise<void> | undefined
let reads = 0, creates = 0, issues = 0
const collector = createCertificateRenewCollector({
getSettings: async () => settings, getTargets: async () => { await targetGate; return [target] }, hasActiveJob: async () => active, getServer: async () => server,
read: async () => { reads++; if (readFailure) throw new Error("RouterOS offline"); return rows },
createJob: async input => { creates++; job = { ...input, serverId: Number(input.serverId), status: "queued", step: "queued", source: "scheduler", requestedAt: new Date().toISOString(), startedAt: null, finishedAt: null, error: null } },
runJob: async () => { issues++; assert(job); job.status = outcome; job.step = outcome; job.error = outcome === "failed" ? "import refused" : null },
getJob: async () => missingResult || !job ? null : { ...job, ...resultPatch }, touchMeta: async () => {}, event: async () => {},
})
return { collector, settings, target, counts: () => ({ reads, creates, issues }), setServer: (v: typeof server) => { server = v }, setRows: (v: typeof rows) => { rows = v }, setOutcome: (v: typeof outcome) => { outcome = v }, failRead: () => { readFailure = true }, setActive: () => { active = true }, rows: () => rows, patchResult: (v: typeof resultPatch) => { resultPatch = v }, hideResult: () => { missingResult = true }, blockTargets: (v: Promise<void>) => { targetGate = v } }
}
for (const outcome of ["done", "failed", "running", "queued"] as const) {
const f = fixture(); f.setOutcome(outcome); const result = await f.collector.run()
assert.equal(result.renewed, outcome === "done" ? 1 : 0)
assert.equal(result.errors.length, outcome === "failed" ? 1 : 0)
assert.equal(result.skippedTargets, outcome === "running" || outcome === "queued" ? 1 : 0)
assert.equal(f.counts().issues, 1); assert.equal(f.collector.getState().running, false)
}
for (const disabled of [null, { id: 1, name: "off", enabled: false }]) {
const f = fixture(); f.setServer(disabled as Awaited<ReturnType<typeof getServerRowByIdString>>)
const result = await f.collector.run(); assert.equal(result.renewed, 0); assert.equal(result.errors.length, 1); assert.deepEqual(f.counts(), { reads: 0, creates: 0, issues: 0 })
}
{
const f = fixture(); f.setRows([{ ...f.rows()[0], "invalid-after": undefined }]); const result = await f.collector.run()
assert.equal(result.targets?.[0].daysLeft, null); assert.equal(result.skippedTargets, 1); assert.equal(f.counts().creates, 0)
}
for (const kind of ["offline", "malformed", "ambiguous"] as const) {
const f = fixture(); if (kind === "offline") f.failRead(); else if (kind === "malformed") f.setRows([{}]); else f.setRows([...f.rows(), { ...f.rows()[0], ".id": "*B" }])
const result = await f.collector.run(); assert.equal(result.renewed, 0); assert.equal(result.errors.length, 1); assert.equal(f.counts().creates, 0)
}
{
const f = fixture(); f.settings.enabled = false; assert.equal((await f.collector.run()).skipped, true); assert.equal(f.counts().reads, 0)
}
{
const f = fixture(); f.setActive(); const result = await f.collector.run(); assert.equal(result.skippedTargets, 1); assert.equal(f.counts().reads, 0)
}
for (const invalid of ["1abc", "", "0", "-1", "1.5", "9007199254740993"]) assert.equal(await getServerRowByIdString(invalid), null)
for (const patch of [{ serverId: 2 }, { certName: "another" }, { source: "manual" as const }, { domainNames: ["another.test"] }, { step: "cleanup" }]) {
const f = fixture(); f.patchResult(patch); const result = await f.collector.run(); assert.equal(result.renewed, 0)
}
{
const f = fixture(); f.hideResult(); const result = await f.collector.run(); assert.equal(result.renewed, 0); assert.match(result.errors[0], /не подтверждён/)
}
{
const f = fixture(); let release!: () => void; f.blockTargets(new Promise<void>(resolve => { release = resolve }))
const first = f.collector.run(); await new Promise(resolve => setImmediate(resolve)); assert.equal(f.collector.getState().running, true)
assert.equal((await f.collector.run()).skipped, true); release(); assert.equal((await first).renewed, 1); assert.equal(f.counts().issues, 1)
}
console.log("Renewal: only matching done counts as renewed; failed/pending/disabled/unknown/ambiguous/read error never dispatch or claim success; invalid IDs do not query DB")
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"
import { appendEvent } from "../modules/events/service/events-service.js"
import type { CertificatesRenewRunSnapshot } from "../types/scheduler-run-snapshot.js"
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
import { mapRosCertificates } from "./certificate-parse.js"
import { readCertificateServerState } from "./certificates-state.js"
import { runCertificateIssueJob } from "./certificate-issue-runner.js"
import {
createIssueJobRecord,
@@ -11,157 +11,198 @@ import {
hasActiveIssueJob,
listManagedCertificateTargets,
touchCertificateRenewRunMeta,
getIssueJobRecord,
} from "./certificates-service.js"
import { MikrotikClient } from "./mikrotik.js"
import { parseJsonArray } from "../db/json.js"
let collecting = false
export function getCertificateRenewCollectorState(): { running: boolean } {
return { running: collecting }
const defaults = {
getSettings: getCertificateRenewSettings,
getTargets: listManagedCertificateTargets,
hasActiveJob: hasActiveIssueJob,
getServer: getServerRowByIdString,
read: (server: NonNullable<Awaited<ReturnType<typeof getServerRowByIdString>>>) => MikrotikClient.fromServer(server).getCertificates(),
createJob: createIssueJobRecord,
runJob: runCertificateIssueJob,
getJob: getIssueJobRecord,
touchMeta: touchCertificateRenewRunMeta,
event: appendEvent,
}
export async function collectCertificatesRenewOnce(): Promise<CertificatesRenewRunSnapshot> {
const sampledAt = new Date().toISOString()
const settings = await getCertificateRenewSettings()
if (collecting) {
return {
export function createCertificateRenewCollector(overrides: Partial<typeof defaults> = {}) {
const deps = { ...defaults, ...overrides }
let collecting = false
function getState(): { running: boolean } {
return { running: collecting }
}
async function run(): Promise<CertificatesRenewRunSnapshot> {
const sampledAt = new Date().toISOString()
const settings = await deps.getSettings()
if (collecting) {
return {
v: SCHEDULER_RUN_SNAPSHOT_VERSION,
job: "certificates_renew",
sampledAt,
skipped: true,
checked: 0,
renewed: 0,
skippedTargets: 0,
errors: [],
}
}
collecting = true
const started = Date.now()
const snapshot: CertificatesRenewRunSnapshot = {
v: SCHEDULER_RUN_SNAPSHOT_VERSION,
job: "certificates_renew",
sampledAt,
skipped: true,
checked: 0,
renewed: 0,
skippedTargets: 0,
errors: [],
}
}
collecting = true
const started = Date.now()
const snapshot: CertificatesRenewRunSnapshot = {
v: SCHEDULER_RUN_SNAPSHOT_VERSION,
job: "certificates_renew",
sampledAt,
checked: 0,
renewed: 0,
skippedTargets: 0,
errors: [],
targets: [],
}
try {
if (!settings.enabled) {
snapshot.skipped = true
return snapshot
targets: [],
}
const targets = await listManagedCertificateTargets()
for (const target of targets) {
snapshot.checked += 1
const item = {
serverId: target.serverId,
certName: target.certName,
action: "ok" as "ok" | "renewed" | "skipped" | "error",
daysLeft: null as number | null,
message: "",
try {
if (!settings.enabled) {
snapshot.skipped = true
return snapshot
}
if (await hasActiveIssueJob(target.serverId, target.certName)) {
item.action = "skipped"
item.message = "Уже выполняется выпуск"
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
await appendEvent({
level: "info",
eventType: "certificates.renew.skipped",
sourceModule: "certificates",
title: "Автообновление пропущено",
message: `${target.certName}: уже выполняется выпуск`,
entityType: "server",
entityId: target.serverId,
})
continue
}
const server = await getServerRowByIdString(target.serverId)
if (!server) {
item.action = "error"
item.message = "Сервер не найден"
snapshot.errors.push(`${target.certName}: сервер не найден`)
snapshot.targets?.push(item)
continue
}
try {
const client = MikrotikClient.fromServer(server)
const rows = await client.getCertificates()
const mapped = mapRosCertificates(server.id, server.name, rows)
const hit = mapped.find((c) => c.name === target.certName)
if (!hit) {
item.action = "error"
item.message = "Сертификат не найден на устройстве"
snapshot.errors.push(`${target.certName}: не найден на ${server.name}`)
snapshot.targets?.push(item)
continue
}
item.daysLeft = hit.daysLeft
if (hit.daysLeft > settings.renewBeforeDays) {
item.action = "ok"
item.message = `До истечения ${hit.daysLeft} дн.`
snapshot.targets?.push(item)
continue
}
const stillOn = await getCertificateRenewSettings()
if (!stillOn.enabled) {
item.action = "skipped"
item.message = "Автообновление выключено"
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
continue
}
const jobId = randomUUID()
await createIssueJobRecord({
id: jobId,
const targets = await deps.getTargets()
for (const target of targets) {
snapshot.checked += 1
const item = {
serverId: target.serverId,
certName: target.certName,
domainNames: target.domainNames,
keyType: target.keyType,
trustStore: target.trustStore,
source: "scheduler",
})
await runCertificateIssueJob(jobId)
snapshot.renewed += 1
item.action = "renewed"
item.message = "Запущено обновление"
snapshot.targets?.push(item)
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
item.action = "error"
item.message = message
snapshot.errors.push(`${target.certName}: ${message}`)
snapshot.targets?.push(item)
}
}
action: "ok" as "ok" | "renewed" | "skipped" | "error",
daysLeft: null as number | null,
message: "",
}
await touchCertificateRenewRunMeta({
lastCollectedAt: sampledAt,
lastDurationMs: Date.now() - started,
lastError: snapshot.errors.length > 0 ? snapshot.errors.join("; ") : null,
})
return snapshot
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
snapshot.fatalError = message
await touchCertificateRenewRunMeta({
lastCollectedAt: sampledAt,
lastDurationMs: Date.now() - started,
lastError: message,
})
return snapshot
} finally {
collecting = false
if (await deps.hasActiveJob(target.serverId, target.certName)) {
item.action = "skipped"
item.message = "Уже выполняется выпуск"
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
await deps.event({
level: "info",
eventType: "certificates.renew.skipped",
sourceModule: "certificates",
title: "Автообновление пропущено",
message: `${target.certName}: уже выполняется выпуск`,
entityType: "server",
entityId: target.serverId,
})
continue
}
const server = await deps.getServer(target.serverId)
if (!server || !server.enabled) {
item.action = "error"
item.message = "Сервер не найден или выключен"
snapshot.errors.push(`${target.certName}: сервер не найден или выключен`)
snapshot.targets?.push(item)
continue
}
try {
const state = await readCertificateServerState(server, () => deps.read(server))
if (state.error) throw new Error(state.error)
const matches = state.certificates.filter(c => c.name === target.certName)
if (matches.length > 1) throw new Error("Имя сертификата неоднозначно")
const hit = matches[0]
if (!hit) {
item.action = "error"
item.message = "Сертификат не найден на устройстве"
snapshot.errors.push(`${target.certName}: не найден на ${server.name}`)
snapshot.targets?.push(item)
continue
}
item.daysLeft = hit.daysLeft
if (hit.daysLeft === null || hit.status === "unknown") {
item.action = "skipped"
item.message = "Срок или состояние сертификата не подтверждены. Обновление не выполнялось."
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
continue
}
if (hit.daysLeft > settings.renewBeforeDays) {
item.action = "ok"
item.message = `До истечения ${hit.daysLeft} дн.`
snapshot.targets?.push(item)
continue
}
const stillOn = await deps.getSettings()
if (!stillOn.enabled) {
item.action = "skipped"
item.message = "Автообновление выключено"
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
continue
}
const jobId = randomUUID()
await deps.createJob({
id: jobId,
serverId: target.serverId,
certName: target.certName,
domainNames: target.domainNames,
keyType: target.keyType,
trustStore: target.trustStore,
source: "scheduler",
})
await deps.runJob(jobId)
const result = await deps.getJob(jobId)
if (!result || result.id !== jobId || String(result.serverId) !== target.serverId || result.certName !== target.certName || result.source !== "scheduler" || JSON.stringify(parseJsonArray(result.domainNames).map(String)) !== JSON.stringify(target.domainNames) || result.keyType !== target.keyType || result.trustStore !== target.trustStore) throw new Error("Результат обновления сертификата не подтверждён")
if (result.status === "failed") throw new Error(result.error || "Задача обновления сертификата завершилась ошибкой")
if (result.status !== "done" || result.step !== "done") {
item.action = "skipped"
item.message = "Результат задачи пока не подтверждён; новый выпуск не запускался."
snapshot.skippedTargets += 1
snapshot.targets?.push(item)
continue
}
snapshot.renewed += 1
item.action = "renewed"
item.message = "Задача обновления завершена"
snapshot.targets?.push(item)
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
item.action = "error"
item.message = message
snapshot.errors.push(`${target.certName}: ${message}`)
snapshot.targets?.push(item)
}
}
await deps.touchMeta({
lastCollectedAt: sampledAt,
lastDurationMs: Date.now() - started,
lastError: snapshot.errors.length > 0 ? snapshot.errors.join("; ") : null,
})
return snapshot
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
snapshot.fatalError = message
await deps.touchMeta({
lastCollectedAt: sampledAt,
lastDurationMs: Date.now() - started,
lastError: message,
})
return snapshot
} finally {
collecting = false
}
}
return { run, getState }
}
const collector = createCertificateRenewCollector()
export const collectCertificatesRenewOnce = collector.run
export const getCertificateRenewCollectorState = collector.getState
+5 -4
View File
@@ -1,4 +1,5 @@
import { desc, eq, inArray } from "drizzle-orm"
import { and, desc, eq, inArray } from "drizzle-orm"
import { parseDbServerId } from "../utils/server-id.js"
import type { CertificateDto, CertificateRenewSettingsDto } from "@mmapp/contracts/certificates"
import { db } from "../db/index.js"
import { parseJsonArray } from "../db/json.js"
@@ -170,9 +171,9 @@ export function toIssueJobDto(row: NonNullable<Awaited<ReturnType<typeof getIssu
}
export async function getServerRowByIdString(serverId: string | number) {
const id = typeof serverId === "number" ? serverId : Number.parseInt(serverId, 10)
if (!Number.isFinite(id)) return null
return (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0] ?? null
const id = parseDbServerId(serverId)
if (id === null) return null
return (await db.select().from(servers).where(and(eq(servers.id, id), eq(servers.enabled, true))).limit(1))[0] ?? null
}
async function getCertificateRenewSettingsRow() {
@@ -1,12 +1,13 @@
import assert from "node:assert/strict"
import { getEnabledServerById } from "./wireguard-live.js"
import { getServerRowByIdString } from "./certificates-service.js"
import { MikrotikClient } from "./mikrotik.js"
import { pool, closePool } from "../db/index.js"
import { initDatabase } from "../db/bootstrap.js"
import { buildApp } from "../index.js"
try {
for (const id of ["srv1", "1abc", "", "0", "-1", "1.5", "9007199254740993", "9007199254740992"]) assert.equal(await getEnabledServerById(id), null)
for (const id of ["srv1", "1abc", "", "0", "-1", "1.5", "9007199254740993", "9007199254740992"]) { assert.equal(await getEnabledServerById(id), null); assert.equal(await getServerRowByIdString(id), null) }
const own = process.env.MMAPP_OWN_TEST_DB
if (own) {
assert(/^mm_accept_(152|156)_\d+$/.test(own))
@@ -22,6 +23,10 @@ try {
for (const type of ["home-router", "jump-host", "exit-node"]) {
await pool.query("UPDATE servers SET type=$1 WHERE id=$2", [type, id])
assert.equal(await getEnabledServerById(id), null)
assert.equal(await getServerRowByIdString(id), null)
const issue = await app.inject({ method: "POST", url: "/api/certificates/issue", payload: { serverId: String(id), certName: "own-disabled", domainNames: ["disabled.example.test"] } })
assert.equal(issue.statusCode, 404); checks++
const catalog = await app.inject({ method: "GET", url: "/api/certificates/state" }); assert.equal(catalog.statusCode, 200); assert.deepEqual(catalog.json().servers, []); checks++
for (const path of ["/wireguard", "/gre/tunnels"]) {
const r = await app.inject({ method: "GET", url: `/api${path}?serverId=${id}` })
assert.equal(r.statusCode, 200)
@@ -46,16 +51,18 @@ try {
checks++
}
}
assert.equal(checks, 33)
assert.equal(checks, 39)
assert.equal(contacts, 0)
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [id])
assert.equal((await getEnabledServerById(id))?.id, id)
assert.equal((await getServerRowByIdString(id))?.id, id)
for (const largeId of [2147483648, Number.MAX_SAFE_INTEGER]) {
await pool.query("INSERT INTO servers (id,name,host,enabled) VALUES ($1,'large-id-fixture','127.0.0.1',true)", [largeId])
assert.equal((await getEnabledServerById(String(largeId)))?.id, largeId)
assert.equal((await getServerRowByIdString(String(largeId)))?.id, largeId)
}
assert.equal(contacts, 0)
console.log("network-disabled-server.test.ts: 33 API/SQL checks for three types, zero RouterOS contacts, enabled positive control passed")
console.log("network-disabled-server.test.ts: 39 API/SQL checks for three types including certificates, zero RouterOS contacts, enabled positive control passed")
console.log("network-disabled-server.test.ts: real bigint IDs above int32 and MAX_SAFE_INTEGER passed without rounding")
} finally { MikrotikClient.fromServer = original; await app.close() }
} else console.log("network-disabled-server.test.ts: invalid IDs passed; own SQL/disabled API checks skipped (MMAPP_OWN_TEST_DB required)")
+13 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап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,18 @@ 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.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
После250 форма явно сообщает: импорт назначает сертификат обоим www-ssl/api-ssl и включает их; trust-store переключатели задают trust-store, не отменяют назначение. Итоговая сводка показывает DNS A-sync как попытку синхронизации с IPv4 сервера и сохранение CNAME. Сетевая логика/API не менялись. Оба trust-store switches получили доступные имена.
+13 -1
View File
@@ -1,9 +1,21 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-14. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## 12.5.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
После250 форма явно сообщает: импорт назначает сертификат обоим www-ssl/api-ssl и включает их; trust-store переключатели задают trust-store, не отменяют назначение. Итоговая сводка показывает DNS A-sync как попытку синхронизации с IPv4 сервера и сохранение CNAME. Сетевая логика/API не менялись. Оба trust-store switches получили доступные имена.
+14 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -36,7 +36,19 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 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 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная12.3.83; внешняя12.5 открыта, завершённые блоки1–252 в записанных scopes. Runtime backend ux-certificates252/frontend ux-certificates251. Общая регрессия249 и профиль сертификатов252 прошли; реальный renewal остаётся открытым. |
## 12.5.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
+16 -4
View File
@@ -1,10 +1,22 @@
# Оставшаяся внешняя приёмка — 2026-10-14
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап12.5 открыт.
Это актуальная матрица после 12.5.251. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Это актуальная матрица после 12.5.252. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-containers249`, frontend `ux-certificates251`; 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-certificates252`, frontend `ux-certificates251`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
## 12.5.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
@@ -242,7 +254,7 @@ Portal offline JWT действует доexp после logout/role change; э
## Следующая последовательность — 2026-10-14
1. **Сертификаты252:** issuance250 и UX/Stepper251 приняты. Исправить disabled/strict-ID и renewal result/unknown-expiry guards, затем профильная проверка. Реальное renewal/CA revoke/remote publishing/multi-instance остаются отдельными scope.
1. **Сертификаты253:** первичный issuance250, UX/Stepper251 и guards/result252 приняты. Подготовить реальную повторную выдачу/renewal с fingerprint/TLS и полным откатом. CA revoke/remote publishing/multi-instance остаются отдельными scope.
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
+13 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -4152,6 +4152,18 @@ 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.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
После250 форма явно сообщает: импорт назначает сертификат обоим www-ssl/api-ssl и включает их; trust-store переключатели задают trust-store, не отменяют назначение. Итоговая сводка показывает DNS A-sync как попытку синхронизации с IPv4 сервера и сохранение CNAME. Сетевая логика/API не менялись. Оба trust-store switches получили доступные имена.
+13 -1
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10
Актуальное продолжение 2026-10-14: 251 — форма выпуска раскрывает включение www-ssl/api-ssl и DNS A-sync, trust-store switches имеют доступные имена. Общий адаптер ReUI Stepper исправляет ARIA/уникальные ID в сертификатах, серверах и бэкапах без правок vendor/styles/motion. 69 unique E2E, TypeScript/lint/build passed; live PC/mobile keyboard/scoped axe4 violations0, writes0. Backend249/frontend ux-certificates251. Следующий252 — disabled/strict-ID guards сертификатов и достоверность результата renewal; реальное renewal ещё открыто. Этап12.5 открыт.
Актуальное продолжение 2026-10-14: 252 — сертификаты используют strict-ID/enabled guards; renewal больше не объявляет failed/pending успешным и не подменяет неизвестный срок нулём. Восемь профильных scripts и 39 API/SQL checks прошли, own DB/role удалены. Backend ux-certificates252/frontend ux-certificates251 healthy, source526/dist1080 matched. Реальный Portal/read1440/390: оба CHR без ошибок, writes0. Следующий253 — подготовка реальной повторной выдачи/renewal и безопасного сравнения сертификата/TLS; эта приёмка ещё открыта. Этап12.5 открыт.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -34,6 +34,18 @@ 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.252 — выключенные серверы и достоверность результата renewal (2026-10-14)
Общий getter сертификатов теперь использует parseDbServerId и SQL enabled=true: numeric prefix/небезопасные ID не доходят до БД, выключенные серверы не возвращаются. Executor повторно отклоняет disabled перед DNS/native вызовами. Renewal использует каноническое наблюдаемое состояние вместо legacy expiry=0: неизвестный срок/статус пропускается, ошибка чтения/неоднозначное имя не запускает выпуск. После runner читается свежая запись задачи и проверяются id/server/name/source/domains/keyType/trustStore; renewed увеличивается только при status=done и step=done. Failed даёт error, queued/running и неподтверждённый результат не дают успеха. Local singleflight сохраняется; атомарная защита нескольких backend не заявляется.
Factory collector позволяет отдельно проверить зависимости без RouterOS/CA. test:certificates включает восемь scripts; новый набор покрывает done/failed/pending, missing/disabled, неизвестный срок, offline/malformed/ambiguous, выключенные настройки, активную задачу, неправильный контекст результата и конкурентный запуск. Executor включает11 сценариев. Локально и в acceptance image профиль прошёл; contracts/backend build, scoped ESLint и diff-check прошли. Полная регрессия249 не повторялась для ограниченного изменения сертификатов.
Расширенный network-disabled-server harness выполнен на собственной SQL БД с отдельной ограниченной ролью:39 API/SQL checks для home-router/jump-host/exit-node, включая certificates issue404/no job и state200/empty, zero RouterOS contacts. Enabled positive control и bigint IDs above int32/MAX_SAFE_INTEGER прошли без округления. После теста контейнер/БД/роль удалены, отсутствие подтверждено. Это выполняет ранее пропущенную152 ветку в текущем scope; monthly149/IPv6 UDP-worker144 этим тестом не покрыты. [Проверки](ux-ui/acceptance/certificates252-regression.json).
Production backend ux-certificates252 развёрнут, healthy; source526 и compiled1080 совпали с локальным снимком. Frontend ux-certificates251 и PostgreSQL/Portal сохранены. [Deployment](ux-ui/acceptance/certificates252-deployment.json). Настоящий Portal/read без mocks: certificate state и renew settings200, оба CHR без ошибки чтения;1440/390 без overflow/JS errors/API writes. Первый запуск Edge не стартовал внутри filesystem sandbox; повтор с разрешённым доступом прошёл, это не дефект приложения. [Live UI](ux-ui/acceptance/certificates252-live-ui.json).
Новых сертификатов, ACME orders, DNS записей, настроек устройств не создавали; native пароли не менялись. Успех fixtures доказывает счётчик/guards, не реальное продление/замену ключа/CA revoke. Следующий253: подготовить конкретную повторную выдачу собственного mmt-домена, сравнение leaf fingerprint и доверенного TLS до/после, восстановление исходных SSL/certs/files/settings/DNS. Выполнение с повторным назначением SSL требует отдельного согласования конкретного сценария; разрешение250 покрывало тот уже очищенный тест.
## 12.5.251 — понятные последствия выпуска и доступные мастера ReUI (2026-10-14)
После250 форма явно сообщает: импорт назначает сертификат обоим www-ssl/api-ssl и включает их; trust-store переключатели задают trust-store, не отменяют назначение. Итоговая сводка показывает DNS A-sync как попытку синхронизации с IPv4 сервера и сохранение CNAME. Сетевая логика/API не менялись. Оба trust-store switches получили доступные имена.
@@ -0,0 +1,17 @@
{
"date": "2026-10-14",
"block": "12.5.252",
"backendImage": "mmapp-test/backend:ux-certificates252",
"frontendImage": "mmapp-test/frontend:ux-certificates251",
"backendHealth": "healthy",
"source": {
"matched": 526
},
"compiled": {
"matched": 1080,
"missing": 0,
"different": 0
},
"composeExit": 0,
"postgresAndPortalRecreated": false
}
@@ -0,0 +1,41 @@
{
"date": "2026-10-14",
"block": "12.5.252",
"realPortalLogin": true,
"noMocks": true,
"results": [
{
"path": "/api/certificates/state",
"status": 200,
"servers": [
{
"serverId": "1",
"error": null,
"certificates": 0
},
{
"serverId": "2",
"error": null,
"certificates": 0
}
]
},
{
"path": "/api/certificates/renew-settings",
"status": 200,
"renewEnabled": true
}
],
"views": [
{
"width": 1440,
"overflow": false
},
{
"width": 390,
"overflow": false
}
],
"pageErrors": [],
"nonAuthApiWrites": 0
}
@@ -0,0 +1,26 @@
{
"date": "2026-10-14",
"block": "12.5.252",
"certificateProfile": {
"scripts": 8,
"local": "passed",
"acceptanceImage": "passed",
"realAcmeRenewal": false
},
"sql": {
"apiChecks": 39,
"serverTypes": [
"home-router",
"jump-host",
"exit-node"
],
"routerOsContacts": 0,
"enabledPositiveControl": "passed",
"bigintIds": "passed",
"ownedDatabaseAndRoleRemoved": true
},
"contractsBuild": "passed",
"backendBuild": "passed",
"scopedLint": "passed",
"fullRegressionNotRepeated": true
}