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: