@@ -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 : "Не удалось сохранить" )