diff --git a/backend/package.json b/backend/package.json index 7f455c67..a6d2470c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,7 +30,7 @@ "test:geoip": "tsx src/services/traffic-flow-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/routes/ospf-optimizer.test.ts", "test:containers": "tsx src/services/containers-state.test.ts && tsx src/routes/containers-state.test.ts", - "test:certificates": "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-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" }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", diff --git a/backend/src/services/certificate-issue-dto.test.ts b/backend/src/services/certificate-issue-dto.test.ts new file mode 100644 index 00000000..07db7eac --- /dev/null +++ b/backend/src/services/certificate-issue-dto.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import type { CertificateIssueJobRow } from "../db/schema.js" +import { mapCertificateIssueJob } from "./certificate-issue-dto.js" + +const row: CertificateIssueJobRow = { id: "job-1", serverId: 1, status: "queued", step: "queued", source: "manual", certName: "cert", domainNames: ["test.example"], keyType: "rsa2048", trustStore: "www,api", requestedAt: "2026-10-01T00:00:00Z", startedAt: null, finishedAt: null, error: null } +test("numeric DB server ID becomes string without altering job target", () => { + const dto = mapCertificateIssueJob(row) + assert.equal(dto.serverId, "1"); assert.deepEqual(dto.domainNames, ["test.example"]); assert.equal(dto.status, "queued") + assert.equal(dto.startedAt, undefined); assert.equal(dto.error, undefined) +}) +test("real failed runner step and error survive strict contract", () => { + const dto = mapCertificateIssueJob({ ...row, status: "failed", step: "failed", error: "RouterOS import failed", finishedAt: "2026-10-01T00:01:00Z" }) + assert.equal(dto.status, "failed"); assert.equal(dto.step, "failed"); assert.equal(dto.error, "RouterOS import failed") +}) +test("legacy JSON domains and done job remain compatible", () => { + const dto = mapCertificateIssueJob({ ...row, status: "done", step: "done", domainNames: JSON.stringify(["test.example"]) }) + assert.deepEqual(dto.domainNames, ["test.example"]); assert.equal(dto.status, "done") +}) +test("invalid state and malformed domain values do not become successful DTOs", () => { + assert.throws(() => mapCertificateIssueJob({ ...row, step: "unexpected" })) + assert.throws(() => mapCertificateIssueJob({ ...row, domainNames: [null] })) +}) diff --git a/backend/src/services/certificate-issue-dto.ts b/backend/src/services/certificate-issue-dto.ts new file mode 100644 index 00000000..a4af36d9 --- /dev/null +++ b/backend/src/services/certificate-issue-dto.ts @@ -0,0 +1,19 @@ +import { certificateIssueJobSchema } from "@mmapp/contracts/certificates" +import type { CertificateIssueJobRow } from "../db/schema.js" +import { parseJsonArray } from "../db/json.js" + +/** PostgreSQL server ID is numeric; HTTP contracts use a string identifier. */ +export function mapCertificateIssueJob(row: CertificateIssueJobRow) { + return certificateIssueJobSchema.parse({ + id: row.id, + status: row.status, + step: row.step, + serverId: String(row.serverId), + certName: row.certName, + domainNames: parseJsonArray(row.domainNames), + requestedAt: row.requestedAt, + startedAt: row.startedAt ?? undefined, + finishedAt: row.finishedAt ?? undefined, + error: row.error ?? undefined, + }) +} diff --git a/backend/src/services/certificates-service.ts b/backend/src/services/certificates-service.ts index a94ef219..4f6512f1 100644 --- a/backend/src/services/certificates-service.ts +++ b/backend/src/services/certificates-service.ts @@ -8,6 +8,7 @@ import { certificateRenewSettings, servers, } from "../db/schema.js" +import { mapCertificateIssueJob } from "./certificate-issue-dto.js" import { mapRosCertificates } from "./certificate-parse.js" import { MikrotikClient } from "./mikrotik.js" @@ -158,19 +159,7 @@ export async function getIssueJobRecord(id: string) { } export function toIssueJobDto(row: NonNullable>>) { - const domainNames = parseJsonArray(row.domainNames).map(String) - return { - id: row.id, - status: row.status, - step: row.step as "queued" | "acme_order" | "dns_challenge" | "finalize" | "import" | "cleanup" | "done", - serverId: row.serverId, - certName: row.certName, - domainNames, - requestedAt: row.requestedAt, - startedAt: row.startedAt ?? undefined, - finishedAt: row.finishedAt ?? undefined, - error: row.error ?? undefined, - } + return mapCertificateIssueJob(row) } export async function getServerRowByIdString(serverId: string | number) { diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 0d7eaa62..1e7652b4 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -226,4 +226,4 @@ REST запрос → ожидание → подтверждённый output / | Пользователи | [Профиль](ux-ui/screens/users-profile-desktop.png), [ошибка](ux-ui/screens/users-error-desktop.png) | [390](ux-ui/screens/users-profile-mobile.png), [320](ux-ui/screens/users-profile-narrow.png) | | Контейнеры | [Обзор](ux-ui/screens/containers-overview-desktop.png), [снимок](ux-ui/screens/containers-export-desktop.png) | [390](ux-ui/screens/containers-export-mobile.png), [320](ux-ui/screens/containers-export-narrow.png) | -Актуальная точка продолжения: 11А.21 — совместимость issue DTO, затем checkpoint/polling выпуска. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Сертификаты и весь 11А ещё не приняты. +Актуальная точка продолжения: 11А.21.2 — отменяемый polling с проверкой цели, затем checkpoint выпуска. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Сертификаты и весь 11А ещё не приняты. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index aa43f0c0..59a8985b 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -27,7 +27,7 @@ | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | -| 11А | Пользователи, контейнеры, сертификаты и резервные копии | В работе: 11А.1–11А.20; пользователи/контейнеры приняты локально, сертификаты: state/URL/API/renew/ACME готовы; далее 11А.21 | +| 11А | Пользователи, контейнеры, сертификаты и резервные копии | В работе: 11А.1–11А.21.1; пользователи/контейнеры приняты локально, сертификаты: state/URL/API/renew/ACME готовы; далее 11А.21.2 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,13 @@ +## Блок 11А.21.1 — совместимость issue DTO (2026-10-01) + +- Реальный backend issue DTO теперь преобразует числовой PostgreSQL serverId в строковый ID контракта; результат проверяется схемой вместо приведения step через as. JSON/JSONB имена доменов поддержаны, malformed значения отклоняются. +- В enum шага добавлен failed, который уже записывает runner. Ошибка задачи и finishedAt сохраняются, а не теряются при frontend strict parse. Сетевой runner/порядок операций и правила прав не менялись. +- 4 DTO unit, backend test:certificates/build, профильный ESLint, повторные 10 frontend API/snapshot unit и frontend TypeScript прошли. +- Далее 11А.21.2: poll helper с отменой и проверкой ожидаемого server/name/domains, затем сохранённый checkpoint на странице и live HTTP fault тесты повторного нажатия/ошибки/смены источника. + ## Блок 11А.20 — достоверная форма ACME (2026-10-01) - ACME форма выделена в собственный компонент с прежней OpsPanel/ReUI композицией. До подтверждённого GET настройки и состояние токена неизвестны; read-only читает фактические значения, сохранение/тест требуют write права. diff --git a/docs/ux-ui-stage-11a.md b/docs/ux-ui-stage-11a.md index 63f3f4bc..aa8444e5 100644 --- a/docs/ux-ui-stage-11a.md +++ b/docs/ux-ui-stage-11a.md @@ -242,3 +242,10 @@ - Save/test имеют общий синхронный lock и блокируют поля на время операции; test не объявляется сохранением. Ошибка Cloudflare видна с причиной. При unmount запросы отменяются, старые ответы/toast не попадают в другой источник. - 10 ACME E2E, 2 поздних PUT context E2E, 2 read-only E2E, TypeScript/ESLint и production frontend прошли. - Следующий блок 11А.21: совместимость реального issue DTO (backend serverId сейчас числом, failed step не включён в контракт), затем сохранение job checkpoint и отмена polling без повторного POST. Выпуск/общая приёмка сертификатов, backups/11Б/12 остаются незавершёнными. + +## Блок 11А.21.1 — совместимость issue DTO (2026-10-01) + +- Реальный backend issue DTO теперь преобразует числовой PostgreSQL serverId в строковый ID контракта; результат проверяется схемой вместо приведения step через as. JSON/JSONB имена доменов поддержаны, malformed значения отклоняются. +- В enum шага добавлен failed, который уже записывает runner. Ошибка задачи и finishedAt сохраняются, а не теряются при frontend strict parse. Сетевой runner/порядок операций и правила прав не менялись. +- 4 DTO unit, backend test:certificates/build, профильный ESLint, повторные 10 frontend API/snapshot unit и frontend TypeScript прошли. +- Далее 11А.21.2: poll helper с отменой и проверкой ожидаемого server/name/domains, затем сохранённый checkpoint на странице и live HTTP fault тесты повторного нажатия/ошибки/смены источника. diff --git a/packages/contracts/src/certificates.ts b/packages/contracts/src/certificates.ts index 5dd92131..3d8f155f 100644 --- a/packages/contracts/src/certificates.ts +++ b/packages/contracts/src/certificates.ts @@ -73,6 +73,7 @@ export const certificateIssueStepSchema = z.enum([ "import", "cleanup", "done", + "failed", ]) export const certificateIssueJobSchema = z.object({