Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
158fc36294 | ||
|
|
7dc4836c71 | ||
|
|
efc3812e12 | ||
|
|
8d5fd84962 | ||
|
|
f69e65b014 | ||
|
|
49d14a00af |
@@ -40,6 +40,7 @@ ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV BACKEND_INTERNAL_URL=http://backend:8000
|
||||
COPY --from=build /app/public ./public
|
||||
COPY --from=build /app/.next/standalone ./
|
||||
COPY --from=build /app/.next/static ./.next/static
|
||||
|
||||
@@ -691,7 +691,8 @@ export default function DashboardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const loadingBlock = probesLoading && liveProbes === null
|
||||
const liveDataPending = liveServersResolved === null || liveProbes === null
|
||||
const loadingBlock = liveDataPending || (probesLoading && liveProbes === null)
|
||||
const srvList = liveServersResolved ?? []
|
||||
const totalSrv = srvList.length
|
||||
const onlineSrv = srvList.filter((s) => s.status === "online").length
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type SchedulerRunRowDto,
|
||||
type UptimeSettingsDto,
|
||||
} from "@/lib/scheduler-settings"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { requestJson, ApiClientError } from "@/shared/api/http-client"
|
||||
import {
|
||||
parseSchedulerRunSnapshot,
|
||||
type AlertEngineRuleDiagSnapshot,
|
||||
@@ -58,15 +58,46 @@ function makeApiFetch(backendUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
function extractApiError(reason: unknown): string {
|
||||
if (reason instanceof ApiClientError) return reason.message
|
||||
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) {
|
||||
const result = results[i]
|
||||
if (result.status === "rejected") {
|
||||
errors.push(`${labels[i]}: ${extractApiError(result.reason)}`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
function Toggle({
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
checked: boolean
|
||||
onChange: (v: boolean) => void
|
||||
disabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors cursor-pointer",
|
||||
"relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors",
|
||||
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
)}
|
||||
>
|
||||
@@ -729,9 +760,41 @@ function RunRowDetail({ r }: { r: SchedulerRunRowDto }) {
|
||||
)
|
||||
}
|
||||
|
||||
type SchedulerToggleOverrides = {
|
||||
trafficEnabled?: boolean
|
||||
serversApiEnabled?: boolean
|
||||
resourcesEnabled?: boolean
|
||||
pingEnabled?: boolean
|
||||
speedEnabled?: boolean
|
||||
internetPathEnabled?: boolean
|
||||
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)
|
||||
}
|
||||
|
||||
export default function DataCollectionPage() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
const isLive = prefsHydrated && mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [trafficCollector, setTrafficCollector] = useState<CollectorSettingsDto | null>(null)
|
||||
@@ -786,7 +849,7 @@ export default function DataCollectionPage() {
|
||||
runFilterJobKey && SCHEDULER_JOB_KEYS.includes(runFilterJobKey as (typeof SCHEDULER_JOB_KEYS)[number])
|
||||
? `?limit=80&jobKey=${encodeURIComponent(runFilterJobKey)}`
|
||||
: "?limit=80"
|
||||
const [traffic, serversApi, uptime, internetPath, certRenew, runsRes] = await Promise.all([
|
||||
const [trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes] = await Promise.allSettled([
|
||||
apiFetch<CollectorSettingsDto>("/api/traffic/settings"),
|
||||
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings"),
|
||||
apiFetch<UptimeSettingsDto>("/api/uptime/settings"),
|
||||
@@ -794,28 +857,71 @@ export default function DataCollectionPage() {
|
||||
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings"),
|
||||
apiFetch<{ runs: SchedulerRunRowDto[] }>(`/api/scheduler/runs${runsQuery}`),
|
||||
])
|
||||
setTrafficCollector(traffic)
|
||||
setServersApiCollector(serversApi)
|
||||
setUptimeCollector(uptime)
|
||||
setInternetPathCollector(internetPath)
|
||||
setSchedulerRuns(runsRes.runs ?? [])
|
||||
setTrafficIntervalDraft(String(traffic.intervalSec))
|
||||
setTrafficRetentionDraft(String(traffic.retentionDays))
|
||||
setUptimeResourceIntervalDraft(String(uptime.intervalSec ?? 300))
|
||||
setUptimeIntervalDraft(String(uptime.probeIntervalSec ?? 15))
|
||||
setUptimeSpeedIntervalDraft(String(uptime.speedIntervalSec ?? 60))
|
||||
setUptimeRetentionDraft(String(uptime.retentionDays))
|
||||
setDraftTrafficEnabled(!!traffic.enabled)
|
||||
setDraftServersApiEnabled(!!serversApi.enabled)
|
||||
setServersApiIntervalDraft(String(serversApi.intervalSec ?? 120))
|
||||
setDraftResourcesEnabled(!!(uptime.resourcesEnabled ?? uptime.enabled))
|
||||
setDraftPingEnabled(!!(uptime.pingEnabled ?? uptime.enabled))
|
||||
setDraftSpeedEnabled(!!(uptime.speedEnabled ?? uptime.enabled))
|
||||
setDraftInternetPathEnabled(!!internetPath.enabled)
|
||||
setInternetPathIntervalDraft(String(internetPath.intervalSec ?? 300))
|
||||
setDraftCertRenewEnabled(!!certRenew.enabled)
|
||||
setCertRenewIntervalDraft(String(certRenew.intervalSec ?? 21600))
|
||||
setRenewBeforeDaysDraft(String(certRenew.renewBeforeDays ?? 30))
|
||||
|
||||
const loadErrors = collectSettledErrors(
|
||||
[trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes],
|
||||
["трафик", "серверы 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))
|
||||
}
|
||||
|
||||
const runsPayload = readSettled(runsRes)
|
||||
if (runsPayload) {
|
||||
setSchedulerRuns(runsPayload.runs ?? [])
|
||||
}
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
|
||||
} finally {
|
||||
@@ -823,6 +929,138 @@ export default function DataCollectionPage() {
|
||||
}
|
||||
}, [apiFetch, isLive, runFilterJobKey])
|
||||
|
||||
const persistSchedulerDrafts = useCallback(async (overrides: SchedulerToggleOverrides = {}) => {
|
||||
const trafficEnabled = overrides.trafficEnabled ?? draftTrafficEnabled
|
||||
const serversApiEnabled = overrides.serversApiEnabled ?? draftServersApiEnabled
|
||||
const resourcesEnabled = overrides.resourcesEnabled ?? draftResourcesEnabled
|
||||
const pingEnabled = overrides.pingEnabled ?? draftPingEnabled
|
||||
const speedEnabled = overrides.speedEnabled ?? draftSpeedEnabled
|
||||
const internetPathEnabled = overrides.internetPathEnabled ?? draftInternetPathEnabled
|
||||
const certRenewEnabled = overrides.certRenewEnabled ?? draftCertRenewEnabled
|
||||
|
||||
const tInt = Math.max(5, Number.parseInt(trafficIntervalDraft, 10) || 30)
|
||||
const tRet = Math.max(1, Number.parseInt(trafficRetentionDraft, 10) || 14)
|
||||
const uRes = Math.max(5, Number.parseInt(uptimeResourceIntervalDraft, 10) || 300)
|
||||
const uPing = Math.max(5, Number.parseInt(uptimeIntervalDraft, 10) || 15)
|
||||
const uSpd = Math.max(10, Number.parseInt(uptimeSpeedIntervalDraft, 10) || 60)
|
||||
const uRet = Math.max(1, Number.parseInt(uptimeRetentionDraft, 10) || 14)
|
||||
const sApiInt = Math.max(10, Number.parseInt(serversApiIntervalDraft, 10) || 120)
|
||||
const ipInt = Math.max(30, Number.parseInt(internetPathIntervalDraft, 10) || 300)
|
||||
const certRenewInt = Math.max(300, Number.parseInt(certRenewIntervalDraft, 10) || 21600)
|
||||
const renewBeforeDays = Math.max(1, Math.min(90, Number.parseInt(renewBeforeDaysDraft, 10) || 30))
|
||||
|
||||
const saveResults = await Promise.allSettled([
|
||||
apiFetch("/api/traffic/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: trafficEnabled,
|
||||
intervalSec: tInt,
|
||||
retentionDays: tRet,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/servers-api-ping/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: serversApiEnabled,
|
||||
intervalSec: sApiInt,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/uptime/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
resourcesEnabled,
|
||||
pingEnabled,
|
||||
speedEnabled,
|
||||
intervalSec: uRes,
|
||||
probeIntervalSec: uPing,
|
||||
speedIntervalSec: uSpd,
|
||||
retentionDays: uRet,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/internet-path/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: internetPathEnabled,
|
||||
intervalSec: ipInt,
|
||||
}),
|
||||
}),
|
||||
apiFetch("/api/certificates/renew-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: certRenewEnabled,
|
||||
intervalSec: certRenewInt,
|
||||
renewBeforeDays,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
const saveErrors = collectSettledErrors(
|
||||
saveResults,
|
||||
["трафик", "серверы REST API", "uptime", "internet path", "сертификаты"],
|
||||
)
|
||||
if (saveErrors.length > 0) {
|
||||
throw new Error(`Не все настройки сохранились: ${saveErrors.join("; ")}`)
|
||||
}
|
||||
}, [
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
draftInternetPathEnabled,
|
||||
draftPingEnabled,
|
||||
draftResourcesEnabled,
|
||||
draftServersApiEnabled,
|
||||
draftSpeedEnabled,
|
||||
draftTrafficEnabled,
|
||||
internetPathIntervalDraft,
|
||||
renewBeforeDaysDraft,
|
||||
serversApiIntervalDraft,
|
||||
trafficIntervalDraft,
|
||||
trafficRetentionDraft,
|
||||
uptimeIntervalDraft,
|
||||
uptimeResourceIntervalDraft,
|
||||
uptimeRetentionDraft,
|
||||
uptimeSpeedIntervalDraft,
|
||||
])
|
||||
|
||||
const handleJobEnabledChange = useCallback(async (jobKey: (typeof SCHEDULER_JOB_KEYS)[number], nextEnabled: boolean) => {
|
||||
const overrides: SchedulerToggleOverrides = {}
|
||||
if (jobKey === "traffic") {
|
||||
setDraftTrafficEnabled(nextEnabled)
|
||||
overrides.trafficEnabled = nextEnabled
|
||||
} else if (jobKey === "servers_rest_ping") {
|
||||
setDraftServersApiEnabled(nextEnabled)
|
||||
overrides.serversApiEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_resources") {
|
||||
setDraftResourcesEnabled(nextEnabled)
|
||||
overrides.resourcesEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_ping") {
|
||||
setDraftPingEnabled(nextEnabled)
|
||||
overrides.pingEnabled = nextEnabled
|
||||
} else if (jobKey === "uptime_speed") {
|
||||
setDraftSpeedEnabled(nextEnabled)
|
||||
overrides.speedEnabled = nextEnabled
|
||||
} else if (jobKey === "certificates_renew") {
|
||||
setDraftCertRenewEnabled(nextEnabled)
|
||||
overrides.certRenewEnabled = nextEnabled
|
||||
} else if (jobKey === "internet_path") {
|
||||
setDraftInternetPathEnabled(nextEnabled)
|
||||
overrides.internetPathEnabled = nextEnabled
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
setCollectorError(null)
|
||||
setSchedulerSaveBusy(true)
|
||||
try {
|
||||
await persistSchedulerDrafts(overrides)
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось сохранить")
|
||||
} finally {
|
||||
setSchedulerSaveBusy(false)
|
||||
}
|
||||
}, [loadCollectors, persistSchedulerDrafts])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
setTrafficCollector(null)
|
||||
@@ -851,6 +1089,9 @@ export default function DataCollectionPage() {
|
||||
}
|
||||
|
||||
const enabledJobsCount = useMemo(() => {
|
||||
const jobs = uptimeCollector?.scheduler?.jobs
|
||||
if (jobs?.length) return jobs.filter((job) => job.enabled).length
|
||||
|
||||
let n = draftTrafficEnabled ? 1 : 0
|
||||
if (draftServersApiEnabled) n += 1
|
||||
if (draftResourcesEnabled) n += 1
|
||||
@@ -867,6 +1108,7 @@ export default function DataCollectionPage() {
|
||||
draftServersApiEnabled,
|
||||
draftSpeedEnabled,
|
||||
draftTrafficEnabled,
|
||||
uptimeCollector?.scheduler?.jobs,
|
||||
])
|
||||
|
||||
const schedulerJobCount = SCHEDULER_JOB_KEYS.length
|
||||
@@ -883,7 +1125,9 @@ export default function DataCollectionPage() {
|
||||
{
|
||||
label: "Включено задач",
|
||||
value: `${enabledJobsCount} / ${schedulerJobCount}`,
|
||||
sub: "По переключателям на этой странице (до сохранения)",
|
||||
sub: uptimeCollector?.scheduler?.jobs?.length
|
||||
? "По сохранённым задачам планировщика"
|
||||
: "По переключателям на этой странице",
|
||||
icon: <CalendarClockIcon className="size-4 text-muted-foreground" />,
|
||||
},
|
||||
{
|
||||
@@ -950,7 +1194,18 @@ export default function DataCollectionPage() {
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex flex-col gap-5 max-w-[1100px] mx-auto w-full">
|
||||
{!isLive && (
|
||||
{!prefsHydrated && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Загрузка настроек подключения</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Читаем режим данных и адрес API из локальных настроек.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{prefsHydrated && !isLive && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Нужен live-режим</CardTitle>
|
||||
@@ -995,7 +1250,7 @@ export default function DataCollectionPage() {
|
||||
<CardHeader className="border-b border-border pb-4">
|
||||
<CardTitle className="text-base">Планировщик сбора данных</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Интервалы и вкл/выкл по задачам. Сохранение отправляет настройки на бекенд и перезапускает таймеры.
|
||||
Интервалы и вкл/выкл по задачам. Переключатель сразу сохраняет задачу на бекенде; кнопка ниже — интервалы и срок хранения.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 pb-0">
|
||||
@@ -1021,15 +1276,15 @@ export default function DataCollectionPage() {
|
||||
? draftTrafficEnabled
|
||||
: jobKey === "servers_rest_ping"
|
||||
? draftServersApiEnabled
|
||||
: jobKey === "uptime_resources"
|
||||
: jobKey === "uptime_resources"
|
||||
? draftResourcesEnabled
|
||||
: jobKey === "uptime_ping"
|
||||
? draftPingEnabled
|
||||
: jobKey === "uptime_speed"
|
||||
? draftSpeedEnabled
|
||||
: jobKey === "certificates_renew"
|
||||
? draftCertRenewEnabled
|
||||
: draftInternetPathEnabled
|
||||
: jobKey === "uptime_speed"
|
||||
? draftSpeedEnabled
|
||||
: jobKey === "certificates_renew"
|
||||
? draftCertRenewEnabled
|
||||
: draftInternetPathEnabled
|
||||
const iv = fixedSchedule
|
||||
? String(j?.intervalSec ?? (jobKey === "gre_bgp" ? 30 : 20))
|
||||
: jobKey === "traffic"
|
||||
@@ -1088,15 +1343,10 @@ export default function DataCollectionPage() {
|
||||
<span className={fixedSchedule ? "inline-flex pointer-events-none opacity-50" : "inline-flex"}>
|
||||
<Toggle
|
||||
checked={en}
|
||||
disabled={fixedSchedule || schedulerSaveBusy}
|
||||
onChange={(v) => {
|
||||
if (fixedSchedule) return
|
||||
if (jobKey === "traffic") setDraftTrafficEnabled(v)
|
||||
else if (jobKey === "servers_rest_ping") setDraftServersApiEnabled(v)
|
||||
else if (jobKey === "uptime_resources") setDraftResourcesEnabled(v)
|
||||
else if (jobKey === "uptime_ping") setDraftPingEnabled(v)
|
||||
else if (jobKey === "uptime_speed") setDraftSpeedEnabled(v)
|
||||
else if (jobKey === "certificates_renew") setDraftCertRenewEnabled(v)
|
||||
else setDraftInternetPathEnabled(v)
|
||||
if (fixedSchedule || schedulerSaveBusy) return
|
||||
void handleJobEnabledChange(jobKey, v)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
@@ -1218,58 +1468,7 @@ export default function DataCollectionPage() {
|
||||
setSchedulerSaveBusy(true)
|
||||
setCollectorError(null)
|
||||
try {
|
||||
const tInt = Math.max(5, Number.parseInt(trafficIntervalDraft, 10) || 30)
|
||||
const tRet = Math.max(1, Number.parseInt(trafficRetentionDraft, 10) || 14)
|
||||
const uRes = Math.max(5, Number.parseInt(uptimeResourceIntervalDraft, 10) || 300)
|
||||
const uPing = Math.max(5, Number.parseInt(uptimeIntervalDraft, 10) || 15)
|
||||
const uSpd = Math.max(10, Number.parseInt(uptimeSpeedIntervalDraft, 10) || 60)
|
||||
const uRet = Math.max(1, Number.parseInt(uptimeRetentionDraft, 10) || 14)
|
||||
const sApiInt = Math.max(10, Number.parseInt(serversApiIntervalDraft, 10) || 120)
|
||||
const ipInt = Math.max(30, Number.parseInt(internetPathIntervalDraft, 10) || 300)
|
||||
await apiFetch("/api/traffic/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: draftTrafficEnabled,
|
||||
intervalSec: tInt,
|
||||
retentionDays: tRet,
|
||||
}),
|
||||
})
|
||||
await apiFetch("/api/servers-api-ping/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: draftServersApiEnabled,
|
||||
intervalSec: sApiInt,
|
||||
}),
|
||||
})
|
||||
await apiFetch("/api/uptime/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
resourcesEnabled: draftResourcesEnabled,
|
||||
pingEnabled: draftPingEnabled,
|
||||
speedEnabled: draftSpeedEnabled,
|
||||
intervalSec: uRes,
|
||||
probeIntervalSec: uPing,
|
||||
speedIntervalSec: uSpd,
|
||||
retentionDays: uRet,
|
||||
}),
|
||||
})
|
||||
const certRenewInt = Math.max(300, Number.parseInt(certRenewIntervalDraft, 10) || 21600)
|
||||
const renewBeforeDays = Math.max(1, Math.min(90, Number.parseInt(renewBeforeDaysDraft, 10) || 30))
|
||||
await apiFetch("/api/internet-path/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: draftInternetPathEnabled,
|
||||
intervalSec: ipInt,
|
||||
}),
|
||||
})
|
||||
await apiFetch("/api/certificates/renew-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: draftCertRenewEnabled,
|
||||
intervalSec: certRenewInt,
|
||||
renewBeforeDays,
|
||||
}),
|
||||
})
|
||||
await persistSchedulerDrafts()
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось сохранить")
|
||||
|
||||
@@ -27,6 +27,7 @@ import { refreshScheduler, stopScheduler } from "./services/scheduler.js"
|
||||
// ── app factory ────────────────────────────────────────────────────────────────
|
||||
|
||||
const app = Fastify({
|
||||
bodyLimit: 512 * 1024 * 1024,
|
||||
logger: {
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
|
||||
@@ -18,8 +18,12 @@ services:
|
||||
image: git.shts.su/denozord/mikrotikmanager-frontend:latest
|
||||
container_name: mmapp-frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
BACKEND_INTERNAL_URL: http://backend:8000
|
||||
labels:
|
||||
mmapp.updater.managed: "true"
|
||||
mmapp.updater.target: frontend
|
||||
|
||||
@@ -321,7 +321,7 @@ wait_for_health() {
|
||||
while (( SECONDS < deadline )); do
|
||||
if [[ "$health_type" == "http" ]]; then
|
||||
local status
|
||||
status="$(curl --silent --output /dev/null --write-out '%{http_code}' --max-time 5 "$health_url" || true)"
|
||||
status="$(curl --silent --location --output /dev/null --write-out '%{http_code}' --max-time 5 "$health_url" || true)"
|
||||
last_status="$status"
|
||||
if [[ "$status" == "$expect_status" ]]; then
|
||||
log info "target=${target_id} action=health_ok status=${status}"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"image": "git.shts.su/denozord/mikrotikmanager-frontend:latest",
|
||||
"health": {
|
||||
"type": "http",
|
||||
"url": "http://frontend:3000/",
|
||||
"url": "http://frontend:3000/dashboard",
|
||||
"expect_status": 200
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -91,9 +91,12 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
|
||||
}, [backendUrlLocked])
|
||||
|
||||
const checkBackend = useCallback(async () => {
|
||||
const url = normalizeBackendUrl(backendUrl)
|
||||
const healthUrl =
|
||||
configuredBackendUrl().kind === "same-origin"
|
||||
? "/health"
|
||||
: `${normalizeBackendUrl(backendUrl)}/health`
|
||||
try {
|
||||
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) })
|
||||
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(3000) })
|
||||
setBackendStatus(res.ok)
|
||||
} catch {
|
||||
setBackendStatus(false)
|
||||
|
||||
@@ -7,6 +7,9 @@ const backendInternalUrl = (process.env.BACKEND_INTERNAL_URL ?? "http://127.0.0.
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
proxyClientMaxBodySize: "512mb",
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{ source: "/health", destination: `${backendInternalUrl}/health` },
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { configuredBackendUrl } from "@/lib/backend-url"
|
||||
|
||||
export class ApiClientError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -13,13 +15,20 @@ function trimBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/$/, "")
|
||||
}
|
||||
|
||||
function resolveRequestUrl(baseUrl: string, path: string): string {
|
||||
if (path.startsWith("/") && configuredBackendUrl().kind === "same-origin") {
|
||||
return path
|
||||
}
|
||||
return trimBaseUrl(baseUrl) + path
|
||||
}
|
||||
|
||||
export async function requestJson<T>(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<T> {
|
||||
const hasBody = init?.body != null
|
||||
const res = await fetch(trimBaseUrl(baseUrl) + path, {
|
||||
const res = await fetch(resolveRequestUrl(baseUrl, path), {
|
||||
...init,
|
||||
headers: {
|
||||
...(hasBody ? { "Content-Type": "application/json" } : {}),
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { ApiClientError } from "@/shared/api/http-client"
|
||||
import { configuredBackendUrl } from "@/lib/backend-url"
|
||||
|
||||
const MAX_RESTORE_BYTES = 512 * 1024 * 1024
|
||||
|
||||
function trimBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/$/, "")
|
||||
}
|
||||
|
||||
function resolveDatabaseApiUrl(baseUrl: string, path: string): string {
|
||||
if (configuredBackendUrl().kind === "same-origin") {
|
||||
return path
|
||||
}
|
||||
return `${trimBaseUrl(baseUrl)}${path}`
|
||||
}
|
||||
|
||||
function parseFilename(contentDisposition: string | null, fallback: string): string {
|
||||
if (!contentDisposition) return fallback
|
||||
const utfMatch = /filename\*=UTF-8''([^;]+)/i.exec(contentDisposition)
|
||||
@@ -22,7 +32,7 @@ function parseFilename(contentDisposition: string | null, fallback: string): str
|
||||
export async function downloadSystemDatabaseBackup(
|
||||
baseUrl: string,
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const res = await fetch(`${trimBaseUrl(baseUrl)}/api/system/database/backup`)
|
||||
const res = await fetch(resolveDatabaseApiUrl(baseUrl, "/api/system/database/backup"))
|
||||
if (!res.ok) {
|
||||
const payload = await res.json().catch(() => undefined)
|
||||
const msg =
|
||||
@@ -40,8 +50,14 @@ export async function downloadSystemDatabaseBackup(
|
||||
}
|
||||
|
||||
export async function restoreSystemDatabaseBackup(baseUrl: string, file: File): Promise<void> {
|
||||
if (file.size > MAX_RESTORE_BYTES) {
|
||||
throw new ApiClientError(
|
||||
`Файл больше ${MAX_RESTORE_BYTES / (1024 * 1024)} МБ — уменьшите бэкап или обратитесь к администратору`,
|
||||
413,
|
||||
)
|
||||
}
|
||||
const body = await file.arrayBuffer()
|
||||
const res = await fetch(`${trimBaseUrl(baseUrl)}/api/system/database/restore`, {
|
||||
const res = await fetch(resolveDatabaseApiUrl(baseUrl, "/api/system/database/restore"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
body,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user