From cdeb97d841f99808e4269024ac39569ecc3cc8d7 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 8 Sep 2026 16:21:50 +0700 Subject: [PATCH] =?UTF-8?q?feat(certificates):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app/(main)/certificates/page.tsx | 5 +- app/(main)/data-collection/page.tsx | 4 + .../services/certificate-renew-collector.ts | 9 + .../certificate-renew-settings.tsx | 184 ++++++++++++++++++ lib/scheduler-settings.ts | 2 +- 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 components/certificates/certificate-renew-settings.tsx diff --git a/app/(main)/certificates/page.tsx b/app/(main)/certificates/page.tsx index 9041a08..15398e0 100644 --- a/app/(main)/certificates/page.tsx +++ b/app/(main)/certificates/page.tsx @@ -11,6 +11,7 @@ import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbar } from "@/components/data-page-toolbar" import { CertificatesDataGrid } from "@/components/data-grids/certificates-data-grid" +import { CertificateRenewSettingsPanel } from "@/components/certificates/certificate-renew-settings" import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid" import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout" import { ALL_SERVERS_ID, type ServerTileItem } from "@/components/server-tile-rail" @@ -243,7 +244,7 @@ function CertPartReference() { return (
@@ -715,6 +716,8 @@ export default function CertificatesPage() { + + {liveReady && ( setRenewBeforeDaysDraft(e.target.value)} className="h-8 text-sm" inputMode="numeric" + disabled={!draftCertRenewEnabled} /> +

+ Вкл/выкл автообновления MM — также на странице «Сертификаты». Не включайте вместе со встроенным ACME RouterOS. +

diff --git a/backend/src/services/certificate-renew-collector.ts b/backend/src/services/certificate-renew-collector.ts index 1c8f2c5..2dd2fa5 100644 --- a/backend/src/services/certificate-renew-collector.ts +++ b/backend/src/services/certificate-renew-collector.ts @@ -113,6 +113,15 @@ export async function collectCertificatesRenewOnce(): Promise(null) + const [lastError, setLastError] = useState(null) + const [loaded, setLoaded] = useState(false) + const [toggleBusy, setToggleBusy] = useState(false) + const [saveBusy, setSaveBusy] = useState(false) + + const load = useCallback(async () => { + if (!liveReady) return + try { + const s = await getCertificateRenewSettings(backendUrl) + setEnabled(s.enabled) + setIntervalDraft(String(s.intervalSec)) + setDaysDraft(String(s.renewBeforeDays)) + setLastCollectedAt(s.lastCollectedAt ?? null) + setLastError(s.lastError ?? null) + setLoaded(true) + } catch (e) { + setLoaded(true) + toast.error(e instanceof Error ? e.message : "Не удалось загрузить настройки автообновления") + } + }, [backendUrl, liveReady]) + + useEffect(() => { + queueMicrotask(() => { + void load() + }) + }, [load]) + + async function handleEnabledChange(next: boolean) { + if (!liveReady || toggleBusy) return + const prev = enabled + setEnabled(next) + setToggleBusy(true) + try { + const saved = await putCertificateRenewSettings(backendUrl, { enabled: next }) + setEnabled(saved.enabled) + toast.success(next ? "Автообновление через MikrotikManager включено" : "Автообновление через MikrotikManager выключено") + } catch (e) { + setEnabled(prev) + toast.error(e instanceof Error ? e.message : "Не удалось сохранить") + } finally { + setToggleBusy(false) + } + } + + async function handleSaveSchedule() { + if (!liveReady || saveBusy) return + const intervalSec = Math.max(300, Number.parseInt(intervalDraft, 10) || 21600) + const renewBeforeDays = Math.max(1, Math.min(90, Number.parseInt(daysDraft, 10) || 30)) + setSaveBusy(true) + try { + const saved = await putCertificateRenewSettings(backendUrl, { intervalSec, renewBeforeDays }) + setIntervalDraft(String(saved.intervalSec)) + setDaysDraft(String(saved.renewBeforeDays)) + toast.success("Расписание автообновления сохранено") + } catch (e) { + toast.error(e instanceof Error ? e.message : "Не удалось сохранить расписание") + } finally { + setSaveBusy(false) + } + } + + const interactionsOff = !liveReady || toggleBusy || (liveReady && !loaded) + + return ( + + {!liveReady ? "нет backend" : enabled ? "Включено" : "Выключено"} + + } + contentClassName="px-5 py-4 flex flex-col gap-4" + > +

+
+

Обновлять сертификаты из MM

+

+ Если ACME уже крутит RouterOS — выключите, чтобы не было двойного перевыпуска. +

+
+ +
+ + {!liveReady ? ( + + Нет подключения к API + + Переключатель станет активен, когда backend доступен. Планировщик читает тот же флаг, что и страница «Сбор данных». + + + ) : enabled ? ( + + Не смешивайте с ACME RouterOS + + MM обновляет только сертификаты, выпущенные через эту страницу. Встроенный Let's Encrypt на + устройстве для тех же имён лучше не включать одновременно. + + + ) : ( + + Обновление отдано RouterOS + + Планировщик MM больше не проверяет срок и не перевыпускает сертификаты. Ручной выпуск и импорт + остаются доступны. + + + )} + +
+
+ + setIntervalDraft(e.target.value)} + disabled={!liveReady || saveBusy} + /> + + + setDaysDraft(e.target.value)} + disabled={!liveReady || saveBusy} + /> + +
+
+ + + Журнал планировщика → + +
+
+ + {lastCollectedAt || lastError ? ( +

+ Последний прогон:{" "} + {lastCollectedAt ? new Date(lastCollectedAt).toLocaleString("ru-RU") : "ещё не было"} + {lastError ? ` · ошибка: ${lastError}` : ""} +

+ ) : null} +
+ ) +} diff --git a/lib/scheduler-settings.ts b/lib/scheduler-settings.ts index 94a2a21..ece36ef 100644 --- a/lib/scheduler-settings.ts +++ b/lib/scheduler-settings.ts @@ -40,7 +40,7 @@ export const SCHEDULER_JOB_DESCRIPTIONS: Record = { gre_bgp: "Опрос GRE-туннелей и BGP-сессий на включённых серверах, запись сэмплов в PostgreSQL для движка оповещений.", certificates_renew: - "Проверка сертификатов, выпущенных через UI, и автообновление через ACME DNS-01 (Cloudflare) до истечения срока.", + "Автообновление сертификатов, выпущенных через UI (ACME DNS-01 / Cloudflare). Отключается на странице «Сертификаты», если ACME ведёт RouterOS.", backups: "Плановые бэкапы RouterOS по расписанию со страницы «Бэкапы»; тик планировщика раз в минуту.", alert_engine: