fix(collectors): разделить загрузку настроек и сохранить черновики
This commit is contained in:
1 parent
dd387cf379
commit
d453f2aec3
6 files changed
+169
-122
No files matched your search
@@ -20,12 +20,12 @@ import {
|
||||
SCHEDULER_JOB_DESCRIPTIONS,
|
||||
SCHEDULER_JOB_KEYS,
|
||||
SCHEDULER_JOB_LABELS,
|
||||
type CollectorSettingsDto,
|
||||
type SchedulerJobStatusDto,
|
||||
type SchedulerStatusDto,
|
||||
type SchedulerRunRowDto,
|
||||
type UptimeSettingsDto,
|
||||
} from "@/lib/scheduler-settings"
|
||||
import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings } from "@/shared/api/collector-settings"
|
||||
import { getCertificateRenewSettings } from "@/shared/api/certificates"
|
||||
import { getSchedulerStatus, listSchedulerRuns } from "@/shared/api/scheduler"
|
||||
import { requestJson, ApiClientError } from "@/shared/api/http-client"
|
||||
import {
|
||||
@@ -77,14 +77,11 @@ function makeApiFetch(backendUrl: string) {
|
||||
|
||||
function extractApiError(reason: unknown): string {
|
||||
if (reason instanceof ApiClientError) return reason.message
|
||||
if (reason instanceof Error && reason.name === "ZodError") return "Настройки не подтверждены: получен некорректный ответ сервера"
|
||||
if (reason instanceof Error) return reason.message
|
||||
return String(reason)
|
||||
}
|
||||
|
||||
function readSettled<T>(result: PromiseSettledResult<T>): T | null {
|
||||
return result.status === "fulfilled" ? result.value : null
|
||||
}
|
||||
|
||||
function collectSettledErrors(results: PromiseSettledResult<unknown>[], labels: string[]): string[] {
|
||||
const errors: string[] = []
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
@@ -532,27 +529,10 @@ type SchedulerToggleOverrides = {
|
||||
certRenewEnabled?: boolean
|
||||
}
|
||||
|
||||
function applySchedulerJobsToDrafts(
|
||||
jobs: SchedulerJobStatusDto[],
|
||||
setters: {
|
||||
setDraftTrafficEnabled: (value: boolean) => void
|
||||
setDraftServersApiEnabled: (value: boolean) => void
|
||||
setDraftResourcesEnabled: (value: boolean) => void
|
||||
setDraftPingEnabled: (value: boolean) => void
|
||||
setDraftSpeedEnabled: (value: boolean) => void
|
||||
setDraftInternetPathEnabled: (value: boolean) => void
|
||||
setDraftCertRenewEnabled: (value: boolean) => void
|
||||
},
|
||||
) {
|
||||
const byKey = Object.fromEntries(jobs.map((job) => [job.jobKey, job])) as Record<string, SchedulerJobStatusDto>
|
||||
if (byKey.traffic) setters.setDraftTrafficEnabled(!!byKey.traffic.enabled)
|
||||
if (byKey.servers_rest_ping) setters.setDraftServersApiEnabled(!!byKey.servers_rest_ping.enabled)
|
||||
if (byKey.uptime_resources) setters.setDraftResourcesEnabled(!!byKey.uptime_resources.enabled)
|
||||
if (byKey.uptime_ping) setters.setDraftPingEnabled(!!byKey.uptime_ping.enabled)
|
||||
if (byKey.uptime_speed) setters.setDraftSpeedEnabled(!!byKey.uptime_speed.enabled)
|
||||
if (byKey.internet_path) setters.setDraftInternetPathEnabled(!!byKey.internet_path.enabled)
|
||||
if (byKey.certificates_renew) setters.setDraftCertRenewEnabled(!!byKey.certificates_renew.enabled)
|
||||
}
|
||||
const SETTINGS_LABELS = { traffic: "Трафик", rest: "Серверы REST API", uptime: "Uptime", internet: "Internet path", cert: "Сертификаты" } as const
|
||||
type SettingsKey = keyof typeof SETTINGS_LABELS
|
||||
type SettingsRead = { known: boolean; error: string | null; receivedAt: string | null }
|
||||
const INITIAL_SETTINGS: Record<SettingsKey, SettingsRead> = Object.fromEntries(Object.keys(SETTINGS_LABELS).map(key => [key, { known: false, error: null, receivedAt: null }])) as Record<SettingsKey, SettingsRead>
|
||||
|
||||
export default function DataCollectionPage() {
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
@@ -565,10 +545,13 @@ function DataCollectionContent() {
|
||||
const isLive = prefsHydrated && mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [trafficCollector, setTrafficCollector] = useState<CollectorSettingsDto | null>(null)
|
||||
const [serversApiCollector, setServersApiCollector] = useState<CollectorSettingsDto | null>(null)
|
||||
const [uptimeCollector, setUptimeCollector] = useState<UptimeSettingsDto | null>(null)
|
||||
const [internetPathCollector, setInternetPathCollector] = useState<CollectorSettingsDto | null>(null)
|
||||
const [trafficCollector, setTrafficCollector] = useState<Awaited<ReturnType<typeof getTrafficCollectorSettings>> | null>(null)
|
||||
const [serversApiCollector, setServersApiCollector] = useState<Awaited<ReturnType<typeof getServersApiPingSettings>> | null>(null)
|
||||
const [uptimeCollector, setUptimeCollector] = useState<Awaited<ReturnType<typeof getUptimeCollectorSettings>> | null>(null)
|
||||
const [internetPathCollector, setInternetPathCollector] = useState<Awaited<ReturnType<typeof getInternetPathSettings>> | null>(null)
|
||||
const [settingsReads, setSettingsReads] = useState(INITIAL_SETTINGS)
|
||||
const dirtySettings = useRef(new Set<SettingsKey>())
|
||||
const settingsReady = Object.values(settingsReads).every(value => value.known && !value.error)
|
||||
const [trafficIntervalDraft, setTrafficIntervalDraft] = useState("30")
|
||||
const [trafficRetentionDraft, setTrafficRetentionDraft] = useState("14")
|
||||
const [uptimeResourceIntervalDraft, setUptimeResourceIntervalDraft] = useState("300")
|
||||
@@ -640,84 +623,51 @@ function DataCollectionContent() {
|
||||
if (current()) { setSchedulerStatus(data); setSchedulerStatusError(null); setSchedulerStatusReceivedAt(new Date().toISOString()) }
|
||||
return data
|
||||
}, error => { if (current()) setSchedulerStatusError(extractApiError(error)); throw error })
|
||||
const [trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes, statusRes] = await Promise.allSettled([
|
||||
apiFetch<CollectorSettingsDto>("/api/traffic/settings", { signal }),
|
||||
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings", { signal }),
|
||||
apiFetch<UptimeSettingsDto>("/api/uptime/settings", { signal }),
|
||||
apiFetch<CollectorSettingsDto>("/api/internet-path/settings", { signal }),
|
||||
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings", { signal }),
|
||||
function publish<T>(key: SettingsKey, request: Promise<T>, accept: (data: T, updateDraft: boolean) => void) {
|
||||
return request.then(data => {
|
||||
if (current()) {
|
||||
accept(data, !dirtySettings.current.has(key))
|
||||
setSettingsReads(prev => ({ ...prev, [key]: { known: true, error: null, receivedAt: new Date().toISOString() } }))
|
||||
}
|
||||
}, error => {
|
||||
if (current()) setSettingsReads(prev => ({ ...prev, [key]: { ...prev[key], error: extractApiError(error) } }))
|
||||
})
|
||||
}
|
||||
await Promise.allSettled([
|
||||
publish("traffic", getTrafficCollectorSettings(backendUrl, { signal }), (data, draft) => {
|
||||
setTrafficCollector(data)
|
||||
if (draft) { setTrafficIntervalDraft(String(data.intervalSec)); setTrafficRetentionDraft(String(data.retentionDays)); setDraftTrafficEnabled(data.enabled) }
|
||||
}),
|
||||
publish("rest", getServersApiPingSettings(backendUrl, { signal }), (data, draft) => {
|
||||
setServersApiCollector(data)
|
||||
if (draft) { setServersApiIntervalDraft(String(data.intervalSec)); setDraftServersApiEnabled(data.enabled) }
|
||||
}),
|
||||
publish("uptime", getUptimeCollectorSettings(backendUrl, { signal }), (data, draft) => {
|
||||
setUptimeCollector(data)
|
||||
if (draft) {
|
||||
setUptimeResourceIntervalDraft(String(data.intervalSec)); setUptimeIntervalDraft(String(data.probeIntervalSec)); setUptimeSpeedIntervalDraft(String(data.speedIntervalSec)); setUptimeRetentionDraft(String(data.retentionDays))
|
||||
setDraftResourcesEnabled(data.resourcesEnabled); setDraftPingEnabled(data.pingEnabled); setDraftSpeedEnabled(data.speedEnabled)
|
||||
}
|
||||
}),
|
||||
publish("internet", getInternetPathSettings(backendUrl, { signal }), (data, draft) => {
|
||||
setInternetPathCollector(data)
|
||||
if (draft) { setInternetPathIntervalDraft(String(data.intervalSec)); setDraftInternetPathEnabled(data.enabled) }
|
||||
}),
|
||||
publish("cert", getCertificateRenewSettings(backendUrl, { signal }), (data, draft) => {
|
||||
if (draft) { setCertRenewIntervalDraft(String(data.intervalSec)); setRenewBeforeDaysDraft(String(data.renewBeforeDays)); setDraftCertRenewEnabled(data.enabled) }
|
||||
}),
|
||||
runsRequest, statusRequest,
|
||||
])
|
||||
|
||||
if (!current()) return
|
||||
const loadErrors = collectSettledErrors(
|
||||
[trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes, statusRes],
|
||||
["трафик", "серверы REST API", "uptime", "internet path", "сертификаты", "журнал планировщика", "статус планировщика"],
|
||||
)
|
||||
if (loadErrors.length > 0) {
|
||||
setCollectorError(loadErrors.join("; "))
|
||||
}
|
||||
|
||||
const traffic = readSettled(trafficRes)
|
||||
if (traffic) {
|
||||
setTrafficCollector(traffic)
|
||||
setTrafficIntervalDraft(String(traffic.intervalSec))
|
||||
setTrafficRetentionDraft(String(traffic.retentionDays))
|
||||
setDraftTrafficEnabled(!!traffic.enabled)
|
||||
}
|
||||
|
||||
const serversApi = readSettled(serversApiRes)
|
||||
if (serversApi) {
|
||||
setServersApiCollector(serversApi)
|
||||
setDraftServersApiEnabled(!!serversApi.enabled)
|
||||
setServersApiIntervalDraft(String(serversApi.intervalSec ?? 120))
|
||||
}
|
||||
|
||||
const uptime = readSettled(uptimeRes)
|
||||
if (uptime) {
|
||||
setUptimeCollector(uptime)
|
||||
setUptimeResourceIntervalDraft(String(uptime.intervalSec ?? 300))
|
||||
setUptimeIntervalDraft(String(uptime.probeIntervalSec ?? 15))
|
||||
setUptimeSpeedIntervalDraft(String(uptime.speedIntervalSec ?? 60))
|
||||
setUptimeRetentionDraft(String(uptime.retentionDays))
|
||||
setDraftResourcesEnabled(!!(uptime.resourcesEnabled ?? uptime.enabled))
|
||||
setDraftPingEnabled(!!(uptime.pingEnabled ?? uptime.enabled))
|
||||
setDraftSpeedEnabled(!!(uptime.speedEnabled ?? uptime.enabled))
|
||||
if (uptime.scheduler?.jobs?.length) {
|
||||
applySchedulerJobsToDrafts(uptime.scheduler.jobs, {
|
||||
setDraftTrafficEnabled,
|
||||
setDraftServersApiEnabled,
|
||||
setDraftResourcesEnabled,
|
||||
setDraftPingEnabled,
|
||||
setDraftSpeedEnabled,
|
||||
setDraftInternetPathEnabled,
|
||||
setDraftCertRenewEnabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const internetPath = readSettled(internetPathRes)
|
||||
if (internetPath) {
|
||||
setInternetPathCollector(internetPath)
|
||||
setDraftInternetPathEnabled(!!internetPath.enabled)
|
||||
setInternetPathIntervalDraft(String(internetPath.intervalSec ?? 300))
|
||||
}
|
||||
|
||||
const certRenew = readSettled(certRenewRes)
|
||||
if (certRenew) {
|
||||
setDraftCertRenewEnabled(!!certRenew.enabled)
|
||||
setCertRenewIntervalDraft(String(certRenew.intervalSec ?? 21600))
|
||||
setRenewBeforeDaysDraft(String(certRenew.renewBeforeDays ?? 30))
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (current()) setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
|
||||
} finally {
|
||||
if (current()) setPageBusy(false)
|
||||
}
|
||||
}, [apiFetch, backendUrl, isLive, runFilterJobKey])
|
||||
}, [backendUrl, isLive, runFilterJobKey])
|
||||
|
||||
const persistSchedulerDrafts = useCallback(async (overrides: SchedulerToggleOverrides = {}) => {
|
||||
if (!settingsReady) throw new Error("Сначала получите подтверждённые настройки всех сборщиков")
|
||||
const trafficEnabled = overrides.trafficEnabled ?? draftTrafficEnabled
|
||||
const serversApiEnabled = overrides.serversApiEnabled ?? draftServersApiEnabled
|
||||
const resourcesEnabled = overrides.resourcesEnabled ?? draftResourcesEnabled
|
||||
@@ -790,6 +740,7 @@ function DataCollectionContent() {
|
||||
throw new Error(`Не все настройки сохранились: ${saveErrors.join("; ")}`)
|
||||
}
|
||||
}, [
|
||||
settingsReady,
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
@@ -811,6 +762,7 @@ function DataCollectionContent() {
|
||||
])
|
||||
|
||||
const handleJobEnabledChange = useCallback(async (jobKey: (typeof SCHEDULER_JOB_KEYS)[number], nextEnabled: boolean) => {
|
||||
if (!settingsReady) return
|
||||
const overrides: SchedulerToggleOverrides = {}
|
||||
if (jobKey === "traffic") {
|
||||
setDraftTrafficEnabled(nextEnabled)
|
||||
@@ -847,7 +799,7 @@ function DataCollectionContent() {
|
||||
} finally {
|
||||
setSchedulerSaveBusy(false)
|
||||
}
|
||||
}, [loadCollectors, persistSchedulerDrafts])
|
||||
}, [loadCollectors, persistSchedulerDrafts, settingsReady])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
@@ -868,7 +820,7 @@ function DataCollectionContent() {
|
||||
const schedulerGridRows = useMemo<SchedulerJobGridRow[]>(() => {
|
||||
return SCHEDULER_JOB_KEYS.map((jobKey) => {
|
||||
const j = schedulerJobsByKey[jobKey]
|
||||
const fixedSchedule = jobKey === "gre_bgp" || jobKey === "alert_engine" || jobKey === "backups"
|
||||
const fixedSchedule = jobKey === "gre_bgp" || jobKey === "alert_engine" || jobKey === "backups" || jobKey === "geoip_update"
|
||||
const enabled = fixedSchedule
|
||||
? Boolean(j?.enabled ?? true)
|
||||
: jobKey === "traffic"
|
||||
@@ -930,16 +882,19 @@ function DataCollectionContent() {
|
||||
? 21600
|
||||
: 300
|
||||
|
||||
const settingsKey: SettingsKey = jobKey === "traffic" ? "traffic" : jobKey === "servers_rest_ping" ? "rest" : jobKey.startsWith("uptime_") ? "uptime" : jobKey === "certificates_renew" ? "cert" : "internet"
|
||||
const settingsKnown = fixedSchedule ? Boolean(j) : settingsReads[settingsKey].known
|
||||
return {
|
||||
id: jobKey,
|
||||
settingsKnown,
|
||||
jobKey,
|
||||
label: SCHEDULER_JOB_LABELS[jobKey] ?? jobKey,
|
||||
description: SCHEDULER_JOB_DESCRIPTIONS[jobKey],
|
||||
fixedSchedule,
|
||||
enabled,
|
||||
intervalValue,
|
||||
intervalValue: settingsKnown ? intervalValue : "",
|
||||
intervalReadOnly: fixedSchedule,
|
||||
intervalDisabled: !enabled && !fixedSchedule,
|
||||
intervalDisabled: !settingsKnown || schedulerSaveBusy || (!enabled && !fixedSchedule),
|
||||
defaultInterval,
|
||||
job: j,
|
||||
onEnabledChange: fixedSchedule
|
||||
@@ -947,7 +902,7 @@ function DataCollectionContent() {
|
||||
: (nextEnabled) => {
|
||||
void handleJobEnabledChange(jobKey, nextEnabled)
|
||||
},
|
||||
onIntervalChange,
|
||||
onIntervalChange: (value: string) => { dirtySettings.current.add(settingsKey); onIntervalChange(value) },
|
||||
onRunNow: async () => {
|
||||
setRunNowJobKey(jobKey)
|
||||
setCollectorError(null)
|
||||
@@ -963,10 +918,11 @@ function DataCollectionContent() {
|
||||
}
|
||||
},
|
||||
runNowLoading: runNowJobKey === jobKey,
|
||||
saveBusy: schedulerSaveBusy,
|
||||
saveBusy: schedulerSaveBusy || !settingsReady,
|
||||
}
|
||||
})
|
||||
}, [
|
||||
settingsReady,
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
@@ -980,6 +936,7 @@ function DataCollectionContent() {
|
||||
internetPathIntervalDraft,
|
||||
loadCollectors,
|
||||
runNowJobKey,
|
||||
settingsReads,
|
||||
schedulerJobsByKey,
|
||||
schedulerSaveBusy,
|
||||
serversApiIntervalDraft,
|
||||
@@ -1013,7 +970,7 @@ function DataCollectionContent() {
|
||||
value: trafficCollector?.lastCollectedAt
|
||||
? new Date(trafficCollector.lastCollectedAt).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit", day: "2-digit", month: "2-digit" })
|
||||
: "—",
|
||||
sub: trafficCollector?.lastError ? trafficCollector.lastError : trafficCollector?.lastDurationMs != null ? `${trafficCollector.lastDurationMs} мс` : "нет данных",
|
||||
sub: settingsReads.traffic.error ? "Последний сбор требует обновления" : trafficCollector?.lastError ? trafficCollector.lastError : trafficCollector?.lastDurationMs != null ? `${trafficCollector.lastDurationMs} мс` : "нет данных",
|
||||
icon: trafficCollector?.lastError ? (
|
||||
<XCircleIcon className="size-4" />
|
||||
) : (
|
||||
@@ -1084,6 +1041,12 @@ function DataCollectionContent() {
|
||||
</Alert>
|
||||
) : null}
|
||||
{isLive && schedulerStatusReceivedAt ? <p className="text-xs text-muted-foreground">Статус получен: {new Date(schedulerStatusReceivedAt).toLocaleString("ru-RU")}</p> : null}
|
||||
{isLive && Object.entries(settingsReads).map(([key, value]) => (
|
||||
<div key={key} className="text-xs text-muted-foreground" data-testid={"settings-" + key}>
|
||||
<p role="status">{SETTINGS_LABELS[key as SettingsKey]} — {value.known ? "настройки получены" : "настройки не получены"}{value.receivedAt ? " (получены): " + new Date(value.receivedAt).toLocaleString("ru-RU") : ""}</p>
|
||||
{value.error ? <Alert variant="warning" className="mt-1"><AlertCircleIcon aria-hidden="true" /><AlertDescription>{value.error}{value.known ? ". Сохранены последние подтверждённые данные." : ""}</AlertDescription></Alert> : null}
|
||||
</div>
|
||||
))}
|
||||
{isLive && collectorError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
@@ -1135,8 +1098,10 @@ function DataCollectionContent() {
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1.5">Хранение сэмплов трафика (дней)</p>
|
||||
<Input
|
||||
value={trafficRetentionDraft}
|
||||
onChange={(e) => setTrafficRetentionDraft(e.target.value)}
|
||||
value={settingsReads.traffic.known ? trafficRetentionDraft : ""}
|
||||
aria-label="Хранение трафика"
|
||||
disabled={!settingsReads.traffic.known || schedulerSaveBusy}
|
||||
onChange={(e) => { dirtySettings.current.add("traffic"); setTrafficRetentionDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
@@ -1144,8 +1109,10 @@ function DataCollectionContent() {
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1.5">Хранение сэмплов uptime (дней)</p>
|
||||
<Input
|
||||
value={uptimeRetentionDraft}
|
||||
onChange={(e) => setUptimeRetentionDraft(e.target.value)}
|
||||
value={settingsReads.uptime.known ? uptimeRetentionDraft : ""}
|
||||
aria-label="Хранение uptime"
|
||||
disabled={!settingsReads.uptime.known || schedulerSaveBusy}
|
||||
onChange={(e) => { dirtySettings.current.add("uptime"); setUptimeRetentionDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
@@ -1153,11 +1120,13 @@ function DataCollectionContent() {
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1.5">Обновлять сертификаты за (дней)</p>
|
||||
<Input
|
||||
value={renewBeforeDaysDraft}
|
||||
onChange={(e) => setRenewBeforeDaysDraft(e.target.value)}
|
||||
value={settingsReads.cert.known ? renewBeforeDaysDraft : ""}
|
||||
aria-label="Обновлять сертификаты за дней"
|
||||
|
||||
onChange={(e) => { dirtySettings.current.add("cert"); setRenewBeforeDaysDraft(e.target.value) }}
|
||||
className="h-8 text-sm"
|
||||
inputMode="numeric"
|
||||
disabled={!draftCertRenewEnabled}
|
||||
disabled={!settingsReads.cert.known || !draftCertRenewEnabled || schedulerSaveBusy}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground mt-1">
|
||||
Вкл/выкл автообновления MM — также на странице «Сертификаты». Не включайте вместе со встроенным ACME RouterOS.
|
||||
@@ -1169,7 +1138,7 @@ function DataCollectionContent() {
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={schedulerSaveBusy}
|
||||
disabled={schedulerSaveBusy || !settingsReady}
|
||||
onClick={async () => {
|
||||
setSchedulerSaveBusy(true)
|
||||
setCollectorError(null)
|
||||
@@ -1200,7 +1169,7 @@ function DataCollectionContent() {
|
||||
) : null}
|
||||
</p>
|
||||
<p className={cn(trafficCollector?.lastError ? "text-destructive" : "")}>
|
||||
{trafficCollector?.lastError ? `Трафик: ${trafficCollector.lastError}` : "Трафик: ошибок нет"}
|
||||
{trafficCollector?.lastError ? `Трафик: ${trafficCollector.lastError}` : (settingsReads.traffic.known ? "Трафик: ошибки не записаны" : "Трафик: состояние неизвестно")}
|
||||
</p>
|
||||
<p>
|
||||
REST API (серверы):{" "}
|
||||
@@ -1217,7 +1186,7 @@ function DataCollectionContent() {
|
||||
<p className={cn(serversApiCollector?.lastError ? "text-destructive" : "")}>
|
||||
{serversApiCollector?.lastError
|
||||
? `REST API (серверы): ${serversApiCollector.lastError}`
|
||||
: "REST API (серверы): ошибок нет"}
|
||||
: (settingsReads.rest.known ? "REST API (серверы): ошибки не записаны" : "REST API (серверы): состояние неизвестно")}
|
||||
</p>
|
||||
<p>
|
||||
Uptime (агрегат в настройках):{" "}
|
||||
@@ -1227,7 +1196,7 @@ function DataCollectionContent() {
|
||||
· {uptimeCollector?.lastDurationMs != null ? `${uptimeCollector.lastDurationMs} мс` : "—"}
|
||||
</p>
|
||||
<p className={cn(uptimeCollector?.lastError ? "text-destructive" : "")}>
|
||||
{uptimeCollector?.lastError ? `Uptime: ${uptimeCollector.lastError}` : "Uptime: ошибок нет"}
|
||||
{uptimeCollector?.lastError ? `Uptime: ${uptimeCollector.lastError}` : (settingsReads.uptime.known ? "Uptime: ошибки не записаны" : "Uptime: состояние неизвестно")}
|
||||
</p>
|
||||
<p>
|
||||
Internet Path snapshot:{" "}
|
||||
@@ -1239,7 +1208,7 @@ function DataCollectionContent() {
|
||||
<p className={cn(internetPathCollector?.lastError ? "text-destructive" : "")}>
|
||||
{internetPathCollector?.lastError
|
||||
? `Internet Path: ${internetPathCollector.lastError}`
|
||||
: "Internet Path: ошибок нет"}
|
||||
: (settingsReads.internet.known ? "Internet Path: ошибки не записаны" : "Internet Path: состояние неизвестно")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user