refactor: replace custom API fetch logic with requestJson utility across multiple pages
Updated the API fetching mechanism in various components to utilize the new requestJson function for improved consistency and error handling. This change affects the alerts, dashboard, data collection, filters, gre, network map, probes, recursive routes, route optimizer, servers, settings, traffic, and uptime pages.
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Flag, countryName } from "@/components/flag"
|
import { Flag, countryName } from "@/components/flag"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
// ─── types ────────────────────────────────────────────────────────────────────
|
// ─── types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -165,17 +166,7 @@ interface BgpSessionListRow {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? res.statusText)
|
|
||||||
}
|
|
||||||
if (res.status === 204) return undefined as T
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,27 +24,12 @@ import { Flag } from "@/components/flag"
|
|||||||
import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, FilterIcon, DownloadIcon } from "lucide-react"
|
import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, FilterIcon, DownloadIcon } from "lucide-react"
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
|
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
|
||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type SchedulerRunRowDto,
|
type SchedulerRunRowDto,
|
||||||
type UptimeSettingsDto,
|
type UptimeSettingsDto,
|
||||||
} from "@/lib/scheduler-settings"
|
} from "@/lib/scheduler-settings"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import {
|
import {
|
||||||
parseSchedulerRunSnapshot,
|
parseSchedulerRunSnapshot,
|
||||||
type AlertEngineRuleDiagSnapshot,
|
type AlertEngineRuleDiagSnapshot,
|
||||||
@@ -49,23 +50,7 @@ import {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = (await res.json()) as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message || "Ошибка запроса")
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import {
|
import {
|
||||||
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
|
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
|
||||||
type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
|
type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
|
||||||
@@ -1491,16 +1492,7 @@ function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!finalRes.ok) {
|
|
||||||
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? finalRes.statusText)
|
|
||||||
}
|
|
||||||
return finalRes.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -5,6 +5,7 @@ import { PageHeader } from "@/components/page-header"
|
|||||||
import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data"
|
import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data"
|
||||||
import type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhGroup, IkeVersion, Server } from "@/lib/data"
|
import type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhGroup, IkeVersion, Server } from "@/lib/data"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -210,16 +211,7 @@ interface GreTunnelsApiResponse {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!finalRes.ok) {
|
|
||||||
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? finalRes.statusText)
|
|
||||||
}
|
|
||||||
return finalRes.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type ServerType,
|
type ServerType,
|
||||||
} from "@/lib/data"
|
} from "@/lib/data"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import {
|
import {
|
||||||
buildGreMapEdges,
|
buildGreMapEdges,
|
||||||
buildServerResourceMap,
|
buildServerResourceMap,
|
||||||
@@ -175,16 +176,7 @@ function apiGreToGreTunnel(t: ApiGreTunnelRow): GreTunnel {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? res.statusText)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
LoaderCircleIcon, AlertCircleIcon,
|
LoaderCircleIcon, AlertCircleIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
// ─── types ────────────────────────────────────────────────────────────────────
|
// ─── types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -66,16 +67,7 @@ const TOOL_META: Record<DiagTool, {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!finalRes.ok) {
|
|
||||||
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? finalRes.statusText)
|
|
||||||
}
|
|
||||||
return finalRes.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useDataSource } from "@/lib/data-source"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { servers as mockServers, type Server } from "@/lib/data"
|
import { servers as mockServers, type Server } from "@/lib/data"
|
||||||
import { PlusIcon, SaveIcon, TrashIcon, SearchIcon, XIcon, PencilIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon, CheckIcon } from "lucide-react"
|
import { PlusIcon, SaveIcon, TrashIcon, SearchIcon, XIcon, PencilIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon, CheckIcon } from "lucide-react"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
interface BackendServer {
|
interface BackendServer {
|
||||||
id: number
|
id: number
|
||||||
@@ -91,23 +92,7 @@ interface RouteGroup {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import { servers } from "@/lib/data"
|
import { servers } from "@/lib/data"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
import {
|
import {
|
||||||
@@ -40,23 +41,7 @@ import {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-96
@@ -5,71 +5,21 @@ import { PageHeader } from "@/components/page-header"
|
|||||||
import { StatusBadge } from "@/components/status-badge"
|
import { StatusBadge } from "@/components/status-badge"
|
||||||
import { servers as initialServers } from "@/lib/data"
|
import { servers as initialServers } from "@/lib/data"
|
||||||
import type { ServerType, Server, WanUplink } from "@/lib/data"
|
import type { ServerType, Server, WanUplink } from "@/lib/data"
|
||||||
|
import type { ServerCreate, ServerUpdate } from "@/packages/contracts/src/servers"
|
||||||
|
import { toFrontendServer } from "@/entities/server/model/mappers"
|
||||||
|
import {
|
||||||
|
createServer,
|
||||||
|
deleteServer,
|
||||||
|
getServer,
|
||||||
|
listServers,
|
||||||
|
pollServer,
|
||||||
|
testServerConnection,
|
||||||
|
updateServer,
|
||||||
|
} from "@/shared/api/servers"
|
||||||
|
|
||||||
// ─── Backend integration ──────────────────────────────────────────────────────
|
// ─── Backend integration ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
|
|
||||||
interface BackendServer {
|
|
||||||
id: number; name: string; host: string; port: number
|
|
||||||
useSsl: boolean; verifySsl: boolean; username: string; password: string
|
|
||||||
type: ServerType; site: string; country: string; asn: string
|
|
||||||
comment: string; enabled: boolean
|
|
||||||
lanSubnet: string
|
|
||||||
wanUplinks: WanUplink[]
|
|
||||||
status: "online" | "offline" | null; latency: number | null
|
|
||||||
os: string | null; model: string | null; uptime: string | null
|
|
||||||
cpuLoad: number | null; freeMemory: number | null; totalMemory: number | null
|
|
||||||
identityName: string | null
|
|
||||||
sessions: number; polledAt: string | null
|
|
||||||
createdAt: string; updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFrontend(s: BackendServer): Server {
|
|
||||||
return {
|
|
||||||
id: String(s.id),
|
|
||||||
name: s.name || s.host,
|
|
||||||
host: s.host,
|
|
||||||
type: s.type,
|
|
||||||
site: s.site,
|
|
||||||
country: s.country,
|
|
||||||
asn: s.asn,
|
|
||||||
model: s.model ?? "—",
|
|
||||||
os: s.os ?? "—",
|
|
||||||
enabled: s.enabled,
|
|
||||||
status: s.status ?? "offline",
|
|
||||||
latency: s.latency != null ? Math.round(s.latency) : null,
|
|
||||||
sessions: s.sessions ?? 0,
|
|
||||||
comment: s.comment || undefined,
|
|
||||||
lanSubnet: s.lanSubnet || undefined,
|
|
||||||
wanUplinks: Array.isArray(s.wanUplinks) && s.wanUplinks.length ? s.wanUplinks : undefined,
|
|
||||||
// carry extra fields needed for expanded view
|
|
||||||
uptime: s.uptime ?? undefined,
|
|
||||||
cpuLoad: s.cpuLoad ?? undefined,
|
|
||||||
freeMemory: s.freeMemory ?? undefined,
|
|
||||||
totalMemory: s.totalMemory ?? undefined,
|
|
||||||
polledAt: s.polledAt ?? undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
||||||
const headers: HeadersInit = init?.body
|
|
||||||
? { "Content-Type": "application/json" }
|
|
||||||
: {}
|
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
|
|
||||||
throw new Error(err.error ?? res.statusText)
|
|
||||||
}
|
|
||||||
// 204 No Content
|
|
||||||
if (res.status === 204) return undefined as T
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -373,7 +323,6 @@ type SheetMode = "add" | "edit"
|
|||||||
export default function ServersPage() {
|
export default function ServersPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live" && backendStatus === true
|
||||||
const apiFetch = makeApiFetch(backendUrl)
|
|
||||||
|
|
||||||
const [serverList, setServerList] = useState<Server[]>(initialServers)
|
const [serverList, setServerList] = useState<Server[]>(initialServers)
|
||||||
const [_backendOk, setBackendOk] = useState(false)
|
const [_backendOk, setBackendOk] = useState(false)
|
||||||
@@ -402,10 +351,10 @@ export default function ServersPage() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
apiFetch<BackendServer[]>("/api/servers")
|
listServers(backendUrl)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setBackendOk(true)
|
setBackendOk(true)
|
||||||
setServerList(data.map(toFrontend))
|
setServerList(data.map(toFrontendServer))
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setBackendOk(false)
|
setBackendOk(false)
|
||||||
@@ -437,7 +386,7 @@ export default function ServersPage() {
|
|||||||
// Fetch full server details (including credentials) from backend
|
// Fetch full server details (including credentials) from backend
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
try {
|
try {
|
||||||
const full = await apiFetch<BackendServer>(`/api/servers/${s.id}`)
|
const full = await getServer(backendUrl, s.id)
|
||||||
setForm(prev => ({
|
setForm(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
username: full.username ?? "",
|
username: full.username ?? "",
|
||||||
@@ -458,7 +407,7 @@ export default function ServersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const payload = {
|
const payload: ServerCreate = {
|
||||||
host: form.host,
|
host: form.host,
|
||||||
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
||||||
username: form.username,
|
username: form.username,
|
||||||
@@ -479,17 +428,12 @@ export default function ServersPage() {
|
|||||||
if (isLive) {
|
if (isLive) {
|
||||||
try {
|
try {
|
||||||
if (sheetMode === "edit" && editingId) {
|
if (sheetMode === "edit" && editingId) {
|
||||||
const updated = await apiFetch<BackendServer>(
|
const updatedPayload: ServerUpdate = payload
|
||||||
`/api/servers/${editingId}`,
|
const updated = await updateServer(backendUrl, editingId, updatedPayload)
|
||||||
{ method: "PUT", body: JSON.stringify(payload) },
|
setServerList(list => list.map(s => s.id === editingId ? toFrontendServer(updated) : s))
|
||||||
)
|
|
||||||
setServerList(list => list.map(s => s.id === editingId ? toFrontend(updated) : s))
|
|
||||||
} else {
|
} else {
|
||||||
const created = await apiFetch<BackendServer>(
|
const created = await createServer(backendUrl, payload)
|
||||||
"/api/servers",
|
setServerList(list => [...list, toFrontendServer(created)])
|
||||||
{ method: "POST", body: JSON.stringify(payload) },
|
|
||||||
)
|
|
||||||
setServerList(list => [...list, toFrontend(created)])
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Ошибка сохранения:", e)
|
console.error("Ошибка сохранения:", e)
|
||||||
@@ -540,7 +484,7 @@ export default function ServersPage() {
|
|||||||
async function handleDelete(id: string) {
|
async function handleDelete(id: string) {
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/api/servers/${id}`, { method: "DELETE" })
|
await deleteServer(backendUrl, id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Ошибка удаления:", e)
|
console.error("Ошибка удаления:", e)
|
||||||
}
|
}
|
||||||
@@ -552,10 +496,10 @@ export default function ServersPage() {
|
|||||||
if (!isLive) return
|
if (!isLive) return
|
||||||
setPollingIds(p => new Set(p).add(id))
|
setPollingIds(p => new Set(p).add(id))
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/api/servers/${id}/poll`, { method: "POST" })
|
await pollServer(backendUrl, id)
|
||||||
// Reload full list to get updated status/version
|
// Reload full list to get updated status/version
|
||||||
const data = await apiFetch<BackendServer[]>("/api/servers")
|
const data = await listServers(backendUrl)
|
||||||
setServerList(data.map(toFrontend))
|
setServerList(data.map(toFrontendServer))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Poll error:", e)
|
console.error("Poll error:", e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -568,9 +512,9 @@ export default function ServersPage() {
|
|||||||
setPollAllBusy(true)
|
setPollAllBusy(true)
|
||||||
try {
|
try {
|
||||||
const ids = serverList.map(s => s.id)
|
const ids = serverList.map(s => s.id)
|
||||||
await Promise.all(ids.map(id => apiFetch(`/api/servers/${id}/poll`, { method: "POST" }).catch(() => {})))
|
await Promise.all(ids.map(id => pollServer(backendUrl, id).catch(() => {})))
|
||||||
const data = await apiFetch<BackendServer[]>("/api/servers")
|
const data = await listServers(backendUrl)
|
||||||
setServerList(data.map(toFrontend))
|
setServerList(data.map(toFrontendServer))
|
||||||
} finally {
|
} finally {
|
||||||
setPollAllBusy(false)
|
setPollAllBusy(false)
|
||||||
}
|
}
|
||||||
@@ -582,20 +526,15 @@ export default function ServersPage() {
|
|||||||
}
|
}
|
||||||
setTestState("testing"); setTestMsg("")
|
setTestState("testing"); setTestMsg("")
|
||||||
try {
|
try {
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + "/api/servers/test-connection", {
|
const data = await testServerConnection(backendUrl, {
|
||||||
method: "POST",
|
host: form.host,
|
||||||
headers: { "Content-Type": "application/json" },
|
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
||||||
body: JSON.stringify({
|
useSsl: form.proto === "https",
|
||||||
host: form.host,
|
verifySsl: form.verifySsl,
|
||||||
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
apiPath: form.apiPath || "/rest",
|
||||||
useSsl: form.proto === "https",
|
username: form.username,
|
||||||
verifySsl: form.verifySsl,
|
password: form.password,
|
||||||
apiPath: form.apiPath || "/rest",
|
|
||||||
username: form.username,
|
|
||||||
password: form.password,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
const data = await res.json() as { success: boolean; message: string }
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTestState("ok"); setTestMsg(data.message)
|
setTestState("ok"); setTestMsg(data.message)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ServerIcon, LayoutDashboardIcon, RefreshCwIcon, CableIcon, LoaderCircleIcon,
|
ServerIcon, LayoutDashboardIcon, RefreshCwIcon, CableIcon, LoaderCircleIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
// ─── types ────────────────────────────────────────────────────────────────────
|
// ─── types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -152,23 +153,7 @@ type NavSection = typeof SECTIONS_NAV[number]
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message || "Ошибка запроса")
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -126,23 +127,7 @@ interface LiveTrafficInterface {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"
|
|||||||
import { servers as mockServers, pingProbes as INIT_PROBES, filters, type Server, type Filter } from "@/lib/data"
|
import { servers as mockServers, pingProbes as INIT_PROBES, filters, type Server, type Filter } from "@/lib/data"
|
||||||
import type { PingProbe } from "@/lib/data"
|
import type { PingProbe } from "@/lib/data"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
import {
|
import {
|
||||||
RefreshCwIcon, PlusIcon, SearchIcon, XIcon,
|
RefreshCwIcon, PlusIcon, SearchIcon, XIcon,
|
||||||
ChevronDownIcon, ChevronRightIcon,
|
ChevronDownIcon, ChevronRightIcon,
|
||||||
@@ -63,23 +64,7 @@ function mockProbesWithSavedStars(base: PingProbe[]): PingProbe[] {
|
|||||||
|
|
||||||
function makeApiFetch(backendUrl: string) {
|
function makeApiFetch(backendUrl: string) {
|
||||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
|
return requestJson<T>(backendUrl, path, init)
|
||||||
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
|
|
||||||
...init,
|
|
||||||
headers: { ...headers, ...(init?.headers ?? {}) },
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
let message = res.statusText
|
|
||||||
try {
|
|
||||||
const err = await res.json() as { error?: string }
|
|
||||||
message = err.error ?? message
|
|
||||||
} catch {
|
|
||||||
const text = await res.text().catch(() => "")
|
|
||||||
if (text) message = text
|
|
||||||
}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { ServerRead, WanUplinkRead } from "../../../types/server.js"
|
||||||
|
import type { ServerRow, SnapshotRow } from "../repository/servers-repository.js"
|
||||||
|
|
||||||
|
function parseWanUplinksJson(raw: string | null | undefined): WanUplinkRead[] {
|
||||||
|
if (raw == null || raw === "") return []
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw) as unknown
|
||||||
|
if (!Array.isArray(data)) return []
|
||||||
|
const out: WanUplinkRead[] = []
|
||||||
|
for (const row of data) {
|
||||||
|
if (typeof row !== "object" || row === null) continue
|
||||||
|
const r = row as Record<string, unknown>
|
||||||
|
out.push({
|
||||||
|
id: typeof r.id === "string" ? r.id : "",
|
||||||
|
name: typeof r.name === "string" ? r.name : "",
|
||||||
|
isp: typeof r.isp === "string" ? r.isp : "",
|
||||||
|
iface: typeof r.iface === "string" ? r.iface : "",
|
||||||
|
ip: typeof r.ip === "string" ? r.ip : "",
|
||||||
|
maxDl: typeof r.maxDl === "number" && Number.isFinite(r.maxDl) ? r.maxDl : 0,
|
||||||
|
maxUl: typeof r.maxUl === "number" && Number.isFinite(r.maxUl) ? r.maxUl : 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toServerRead(server: ServerRow, snap: SnapshotRow | undefined): ServerRead {
|
||||||
|
return {
|
||||||
|
id: server.id,
|
||||||
|
name: server.name || server.host,
|
||||||
|
host: server.host,
|
||||||
|
port: server.port,
|
||||||
|
useSsl: server.useSsl,
|
||||||
|
verifySsl: server.verifySsl,
|
||||||
|
username: server.username,
|
||||||
|
password: server.password,
|
||||||
|
type: server.type,
|
||||||
|
site: server.site,
|
||||||
|
country: server.country,
|
||||||
|
asn: server.asn,
|
||||||
|
comment: server.comment,
|
||||||
|
enabled: server.enabled,
|
||||||
|
lanSubnet: server.lanSubnet ?? "",
|
||||||
|
wanUplinks: parseWanUplinksJson(server.wanUplinks),
|
||||||
|
createdAt: server.createdAt,
|
||||||
|
updatedAt: server.updatedAt,
|
||||||
|
status: snap ? snap.status : null,
|
||||||
|
latency: snap?.latencyMs ?? null,
|
||||||
|
os: snap?.rosVersion ?? null,
|
||||||
|
model: snap?.boardName ?? null,
|
||||||
|
uptime: snap?.uptime ?? null,
|
||||||
|
cpuLoad: snap?.cpuLoad ?? null,
|
||||||
|
freeMemory: snap?.freeMemory ?? null,
|
||||||
|
totalMemory: snap?.totalMemory ?? null,
|
||||||
|
identityName: snap?.identityName ?? null,
|
||||||
|
sessions: 0,
|
||||||
|
polledAt: snap?.polledAt ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { desc, eq } from "drizzle-orm"
|
||||||
|
import { db } from "../../../db/index.js"
|
||||||
|
import { serverSnapshots, servers } from "../../../db/schema.js"
|
||||||
|
|
||||||
|
export type ServerRow = typeof servers.$inferSelect
|
||||||
|
export type SnapshotRow = typeof serverSnapshots.$inferSelect
|
||||||
|
|
||||||
|
export function listServerRows(): ServerRow[] {
|
||||||
|
return db.select().from(servers).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerRowById(id: number): ServerRow | undefined {
|
||||||
|
return db.select().from(servers).where(eq(servers.id, id)).limit(1).all()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServerRow(
|
||||||
|
values: Omit<typeof servers.$inferInsert, "id">,
|
||||||
|
): ServerRow {
|
||||||
|
const [inserted] = db.insert(servers).values(values).returning().all()
|
||||||
|
return inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateServerRowById(
|
||||||
|
id: number,
|
||||||
|
values: Partial<ServerRow>,
|
||||||
|
): ServerRow {
|
||||||
|
const [updated] = db.update(servers).set(values).where(eq(servers.id, id)).returning().all()
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteServerRowById(id: number): void {
|
||||||
|
db.delete(servers).where(eq(servers.id, id)).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSnapshotsByServerId(serverId: number, limit: number): SnapshotRow[] {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(serverSnapshots)
|
||||||
|
.where(eq(serverSnapshots.serverId, serverId))
|
||||||
|
.orderBy(desc(serverSnapshots.polledAt))
|
||||||
|
.limit(limit)
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestSnapshot(serverId: number): SnapshotRow | undefined {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(serverSnapshots)
|
||||||
|
.where(eq(serverSnapshots.serverId, serverId))
|
||||||
|
.orderBy(desc(serverSnapshots.polledAt))
|
||||||
|
.limit(1)
|
||||||
|
.all()[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { toSnapshotRead } from "../../../services/poller.js"
|
||||||
|
import {
|
||||||
|
createServerRow,
|
||||||
|
deleteServerRowById,
|
||||||
|
getLatestSnapshot,
|
||||||
|
getServerRowById,
|
||||||
|
listServerRows,
|
||||||
|
listSnapshotsByServerId,
|
||||||
|
updateServerRowById,
|
||||||
|
} from "../repository/servers-repository.js"
|
||||||
|
import { toServerRead } from "../mapper/servers-mapper.js"
|
||||||
|
import type { ServerCreate, ServerRead, ServerUpdate, SnapshotRead } from "../../../types/server.js"
|
||||||
|
|
||||||
|
export function listServersRead(): ServerRead[] {
|
||||||
|
return listServerRows().map((server) => toServerRead(server, getLatestSnapshot(server.id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerReadById(id: number): ServerRead | undefined {
|
||||||
|
const row = getServerRowById(id)
|
||||||
|
if (!row) return undefined
|
||||||
|
return toServerRead(row, getLatestSnapshot(row.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServer(input: ServerCreate): ServerRead {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const { wanUplinks, ...rest } = input
|
||||||
|
const inserted = createServerRow({
|
||||||
|
...rest,
|
||||||
|
wanUplinks: JSON.stringify(wanUplinks ?? []),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
return toServerRead(inserted, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateServer(id: number, input: ServerUpdate): ServerRead {
|
||||||
|
const { wanUplinks, ...rest } = input
|
||||||
|
const setPayload: Record<string, unknown> = { updatedAt: new Date().toISOString() }
|
||||||
|
for (const [k, v] of Object.entries(rest)) {
|
||||||
|
if (v !== undefined) setPayload[k] = v
|
||||||
|
}
|
||||||
|
if (wanUplinks !== undefined) {
|
||||||
|
setPayload.wanUplinks = JSON.stringify(wanUplinks)
|
||||||
|
}
|
||||||
|
const updated = updateServerRowById(id, setPayload as never)
|
||||||
|
return toServerRead(updated, getLatestSnapshot(updated.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteServer(id: number): void {
|
||||||
|
deleteServerRowById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listServerSnapshots(id: number, limit: number): SnapshotRead[] {
|
||||||
|
return listSnapshotsByServerId(id, limit).map(toSnapshotRead)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
import { z } from "zod"
|
|
||||||
import {
|
import {
|
||||||
getAlertsMeta,
|
getAlertsMeta,
|
||||||
getTelegramBotToken,
|
getTelegramBotToken,
|
||||||
@@ -13,90 +12,11 @@ import {
|
|||||||
sendTelegramAlertMessage,
|
sendTelegramAlertMessage,
|
||||||
updateTelegramSettings,
|
updateTelegramSettings,
|
||||||
} from "../services/alerts-service.js"
|
} from "../services/alerts-service.js"
|
||||||
|
import {
|
||||||
const AlertTypeSchema = z.enum([
|
putRulesSchema,
|
||||||
"gre-tunnel",
|
putTelegramSchema,
|
||||||
"bgp-peer",
|
testTelegramSchema,
|
||||||
"bgp-prefix",
|
} from "../../../packages/contracts/dist/alerts.js"
|
||||||
"gre-client",
|
|
||||||
"server",
|
|
||||||
"rtt",
|
|
||||||
"loss",
|
|
||||||
"traffic",
|
|
||||||
])
|
|
||||||
const AlertSeveritySchema = z.enum(["critical", "warning", "info"])
|
|
||||||
const AlertCooldownSchema = z.enum(["1м", "5м", "15м", "1ч", "4ч", "24ч"])
|
|
||||||
const RecoveryModeSchema = z.enum(["always", "never", "conditional"])
|
|
||||||
|
|
||||||
const AlertGroupBodySchema = z.object({
|
|
||||||
id: z.string().min(1),
|
|
||||||
name: z.string().min(1),
|
|
||||||
combineMode: z.enum(["any", "all"]),
|
|
||||||
enabled: z.boolean(),
|
|
||||||
cooldownOverride: z.union([AlertCooldownSchema, z.null()]),
|
|
||||||
})
|
|
||||||
|
|
||||||
const AlertRuleBodySchema = z
|
|
||||||
.object({
|
|
||||||
id: z.string().min(1),
|
|
||||||
name: z.string().min(1),
|
|
||||||
type: AlertTypeSchema,
|
|
||||||
target: z.string().optional(),
|
|
||||||
targets: z.array(z.string().min(1)).optional(),
|
|
||||||
groupId: z.string().nullable().optional(),
|
|
||||||
/** Сводка; если переданы `conditions`, может дублировать первую строку */
|
|
||||||
condition: z.string().optional(),
|
|
||||||
/** Несколько условий (OR); приоритет над одной строкой `condition` */
|
|
||||||
conditions: z.array(z.string().min(1)).optional(),
|
|
||||||
severity: AlertSeveritySchema,
|
|
||||||
enabled: z.boolean(),
|
|
||||||
cooldown: AlertCooldownSchema,
|
|
||||||
/** Секунды подтверждения стабильности; 0 / null / не передано — выкл. */
|
|
||||||
confirmStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
|
||||||
recoveryMode: RecoveryModeSchema.optional(),
|
|
||||||
recoveryStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
|
||||||
chatId: z.string(),
|
|
||||||
})
|
|
||||||
.refine((r) => (r.targets != null && r.targets.length > 0) || Boolean(r.target?.trim()), {
|
|
||||||
message: "Нужен хотя бы один объект: targets или target",
|
|
||||||
path: ["targets"],
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(r) =>
|
|
||||||
(r.conditions != null && r.conditions.length > 0) || Boolean(r.condition?.trim()),
|
|
||||||
{ message: "Нужно хотя бы одно условие: conditions или condition", path: ["conditions"] },
|
|
||||||
)
|
|
||||||
|
|
||||||
const PutRulesSchema = z.object({
|
|
||||||
rules: z.array(AlertRuleBodySchema),
|
|
||||||
groups: z.array(AlertGroupBodySchema).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const PutTelegramSchema = z.object({
|
|
||||||
/** undefined — не менять; null или "" — очистить токен */
|
|
||||||
token: z.union([z.string(), z.null()]).optional(),
|
|
||||||
chatId: z.string().optional(),
|
|
||||||
/** undefined — не менять; null — сбросить (общий чат супергруппы) */
|
|
||||||
messageThreadId: z.union([z.number().int().positive(), z.null()]).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const RulePreviewTelegramSchema = z.object({
|
|
||||||
name: z.string().min(1),
|
|
||||||
targets: z.array(z.string().min(1)).min(1),
|
|
||||||
conditionLine: z.string().min(1),
|
|
||||||
severity: AlertSeveritySchema,
|
|
||||||
cooldown: AlertCooldownSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
const TestTelegramSchema = z.object({
|
|
||||||
/** Если не передан — взять сохранённый токен из БД */
|
|
||||||
token: z.string().optional(),
|
|
||||||
chatId: z.string().optional(),
|
|
||||||
/** Если не передан — из БД; для проверки черновика до сохранения */
|
|
||||||
messageThreadId: z.coerce.number().int().positive().optional(),
|
|
||||||
/** Если задан — вместо короткого текста отправляется предпросмотр правила (создание / правка). */
|
|
||||||
rulePreview: RulePreviewTelegramSchema.optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
app.get("/alerts", async (_req, reply) => {
|
app.get("/alerts", async (_req, reply) => {
|
||||||
@@ -112,7 +32,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.put("/alerts/rules", async (req, reply) => {
|
app.put("/alerts/rules", async (req, reply) => {
|
||||||
const parsed = PutRulesSchema.safeParse(req.body)
|
const parsed = putRulesSchema.safeParse(req.body)
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||||
}
|
}
|
||||||
@@ -152,7 +72,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.put("/alerts/telegram", async (req, reply) => {
|
app.put("/alerts/telegram", async (req, reply) => {
|
||||||
const parsed = PutTelegramSchema.safeParse(req.body)
|
const parsed = putTelegramSchema.safeParse(req.body)
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||||
}
|
}
|
||||||
@@ -166,7 +86,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.post("/alerts/telegram/test", async (req, reply) => {
|
app.post("/alerts/telegram/test", async (req, reply) => {
|
||||||
const parsed = TestTelegramSchema.safeParse(req.body ?? {})
|
const parsed = testTelegramSchema.safeParse(req.body ?? {})
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { db } from "../db/index.js"
|
|||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
import { parseBgpSessions } from "../services/bgp-parse-sessions.js"
|
import { parseBgpSessions } from "../services/bgp-parse-sessions.js"
|
||||||
import { MikrotikClient } from "../services/mikrotik.js"
|
import { MikrotikClient } from "../services/mikrotik.js"
|
||||||
import { ServerIdParamSchema } from "../types/server.js"
|
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||||
import type { BgpSessionRead } from "../types/server.js"
|
import type { BgpSessionRead } from "../types/server.js"
|
||||||
|
|
||||||
const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||||
|
|
||||||
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
|
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
|
||||||
app.get("/bgp/sessions/raw/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.get("/bgp/sessions/raw/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
const server = db.select().from(servers).where(eq(servers.id, req.params.id)).limit(1).all()[0]
|
const params = req.params as ServerIdParams
|
||||||
|
const server = db.select().from(servers).where(eq(servers.id, params.id)).limit(1).all()[0]
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
try {
|
try {
|
||||||
const client = MikrotikClient.fromServer(server)
|
const client = MikrotikClient.fromServer(server)
|
||||||
@@ -44,9 +45,10 @@ const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
|
|
||||||
// GET /api/servers/:id/bgp/sessions — single server
|
// GET /api/servers/:id/bgp/sessions — single server
|
||||||
app.get("/servers/:id/bgp/sessions", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.get("/servers/:id/bgp/sessions", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
const server = db
|
const server = db
|
||||||
.select().from(servers)
|
.select().from(servers)
|
||||||
.where(eq(servers.id, req.params.id))
|
.where(eq(servers.id, params.id))
|
||||||
.limit(1).all()[0]
|
.limit(1).all()[0]
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { z } from "zod"
|
|||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
import { MikrotikClient } from "../services/mikrotik.js"
|
import { MikrotikClient } from "../services/mikrotik.js"
|
||||||
import { ServerIdParamSchema } from "../types/server.js"
|
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||||
import type {
|
import type {
|
||||||
RosIpAddress, RosInterface, RosResource, RosIdentity, RosIpRoute,
|
RosIpAddress, RosInterface, RosResource, RosIdentity, RosIpRoute,
|
||||||
RosFirewallFilter, RosLogEntry, RosBgpSession, RosPingResult,
|
RosFirewallFilter, RosLogEntry, RosBgpSession, RosPingResult,
|
||||||
@@ -313,9 +313,10 @@ const execRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
"/servers/:id/exec",
|
"/servers/:id/exec",
|
||||||
{ schema: { params: ServerIdParamSchema, body: ExecBodySchema } },
|
{ schema: { params: ServerIdParamSchema, body: ExecBodySchema } },
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
const server = db
|
const server = db
|
||||||
.select().from(servers)
|
.select().from(servers)
|
||||||
.where(eq(servers.id, req.params.id))
|
.where(eq(servers.id, params.id))
|
||||||
.limit(1).all()[0]
|
.limit(1).all()[0]
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
|||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { serverSnapshots, servers, trafficSamples, uptimeSpeedProbes } from "../db/schema.js"
|
import { serverSnapshots, servers, trafficSamples, uptimeSpeedProbes } from "../db/schema.js"
|
||||||
import { MikrotikClient } from "../services/mikrotik.js"
|
import { MikrotikClient } from "../services/mikrotik.js"
|
||||||
import { ServerIdParamSchema } from "../types/server.js"
|
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||||
import type {
|
import type {
|
||||||
RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
|
RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
|
||||||
RosBfdSession,
|
RosBfdSession,
|
||||||
@@ -630,9 +630,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
|
|
||||||
// GET /api/servers/:id/ospf — single server OSPF + BFD data
|
// GET /api/servers/:id/ospf — single server OSPF + BFD data
|
||||||
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
const server = db
|
const server = db
|
||||||
.select().from(servers)
|
.select().from(servers)
|
||||||
.where(eq(servers.id, req.params.id))
|
.where(eq(servers.id, params.id))
|
||||||
.limit(1).all()[0]
|
.limit(1).all()[0]
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
@@ -657,9 +658,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
"/servers/:id/ospf/optimize/preview",
|
"/servers/:id/ospf/optimize/preview",
|
||||||
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
const server = db
|
const server = db
|
||||||
.select().from(servers)
|
.select().from(servers)
|
||||||
.where(eq(servers.id, req.params.id))
|
.where(eq(servers.id, params.id))
|
||||||
.limit(1).all()[0]
|
.limit(1).all()[0]
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
@@ -688,9 +690,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
"/servers/:id/ospf/optimize",
|
"/servers/:id/ospf/optimize",
|
||||||
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
const server = db
|
const server = db
|
||||||
.select().from(servers)
|
.select().from(servers)
|
||||||
.where(eq(servers.id, req.params.id))
|
.where(eq(servers.id, params.id))
|
||||||
.limit(1).all()[0]
|
.limit(1).all()[0]
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
|||||||
+36
-161
@@ -1,95 +1,28 @@
|
|||||||
import { desc, eq } from "drizzle-orm"
|
|
||||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
import { db } from "../db/index.js"
|
|
||||||
import { servers, serverSnapshots } from "../db/schema.js"
|
|
||||||
import type { ServerRead, WanUplinkRead } from "../types/server.js"
|
|
||||||
import {
|
import {
|
||||||
ServerCreateSchema,
|
ServerCreateSchema,
|
||||||
ServerUpdateSchema,
|
ServerUpdateSchema,
|
||||||
ServerIdParamSchema,
|
ServerIdParamSchema,
|
||||||
SnapshotsQuerySchema,
|
SnapshotsQuerySchema,
|
||||||
TestConnectionSchema,
|
TestConnectionSchema,
|
||||||
|
type ServerCreateRequest,
|
||||||
|
type ServerIdParams,
|
||||||
|
type ServerUpdateRequest,
|
||||||
|
type SnapshotsQuery,
|
||||||
|
type TestConnectionRequest,
|
||||||
} from "../types/server.js"
|
} from "../types/server.js"
|
||||||
import { pollServer, toSnapshotRead } from "../services/poller.js"
|
import { pollServer } from "../services/poller.js"
|
||||||
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
||||||
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
|
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
|
||||||
|
import {
|
||||||
// ── helpers ────────────────────────────────────────────────────────────────────
|
createServer,
|
||||||
|
deleteServer,
|
||||||
type ServerRow = typeof servers.$inferSelect
|
getServerReadById,
|
||||||
type SnapshotRow = typeof serverSnapshots.$inferSelect
|
listServerSnapshots,
|
||||||
|
listServersRead,
|
||||||
function parseWanUplinksJson(raw: string | null | undefined): WanUplinkRead[] {
|
updateServer,
|
||||||
if (raw == null || raw === "") return []
|
} from "../modules/servers/service/servers-service.js"
|
||||||
try {
|
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
|
||||||
const data = JSON.parse(raw) as unknown
|
|
||||||
if (!Array.isArray(data)) return []
|
|
||||||
const out: WanUplinkRead[] = []
|
|
||||||
for (const row of data) {
|
|
||||||
if (typeof row !== "object" || row === null) continue
|
|
||||||
const r = row as Record<string, unknown>
|
|
||||||
out.push({
|
|
||||||
id: typeof r.id === "string" ? r.id : "",
|
|
||||||
name: typeof r.name === "string" ? r.name : "",
|
|
||||||
isp: typeof r.isp === "string" ? r.isp : "",
|
|
||||||
iface: typeof r.iface === "string" ? r.iface : "",
|
|
||||||
ip: typeof r.ip === "string" ? r.ip : "",
|
|
||||||
maxDl: typeof r.maxDl === "number" && Number.isFinite(r.maxDl) ? r.maxDl : 0,
|
|
||||||
maxUl: typeof r.maxUl === "number" && Number.isFinite(r.maxUl) ? r.maxUl : 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Merge a server row with its latest snapshot into the frontend-compatible shape */
|
|
||||||
function toServerRead(server: ServerRow, snap: SnapshotRow | undefined): ServerRead {
|
|
||||||
return {
|
|
||||||
id: server.id,
|
|
||||||
name: server.name || server.host,
|
|
||||||
host: server.host,
|
|
||||||
port: server.port,
|
|
||||||
useSsl: server.useSsl,
|
|
||||||
verifySsl: server.verifySsl,
|
|
||||||
username: server.username,
|
|
||||||
password: server.password,
|
|
||||||
type: server.type,
|
|
||||||
site: server.site,
|
|
||||||
country: server.country,
|
|
||||||
asn: server.asn,
|
|
||||||
comment: server.comment,
|
|
||||||
enabled: server.enabled,
|
|
||||||
lanSubnet: server.lanSubnet ?? "",
|
|
||||||
wanUplinks: parseWanUplinksJson(server.wanUplinks),
|
|
||||||
createdAt: server.createdAt,
|
|
||||||
updatedAt: server.updatedAt,
|
|
||||||
// snapshot fields (null if never polled)
|
|
||||||
status: snap ? snap.status : null,
|
|
||||||
latency: snap?.latencyMs ?? null,
|
|
||||||
os: snap?.rosVersion ?? null,
|
|
||||||
model: snap?.boardName ?? null,
|
|
||||||
uptime: snap?.uptime ?? null,
|
|
||||||
cpuLoad: snap?.cpuLoad ?? null,
|
|
||||||
freeMemory: snap?.freeMemory ?? null,
|
|
||||||
totalMemory: snap?.totalMemory ?? null,
|
|
||||||
identityName: snap?.identityName ?? null,
|
|
||||||
sessions: 0,
|
|
||||||
polledAt: snap?.polledAt ?? null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get latest snapshot for a single server */
|
|
||||||
function getLatestSnapshot(serverId: number): SnapshotRow | undefined {
|
|
||||||
return db
|
|
||||||
.select()
|
|
||||||
.from(serverSnapshots)
|
|
||||||
.where(eq(serverSnapshots.serverId, serverId))
|
|
||||||
.orderBy(desc(serverSnapshots.polledAt))
|
|
||||||
.limit(1)
|
|
||||||
.all()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── plugin ─────────────────────────────────────────────────────────────────────
|
// ── plugin ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -97,7 +30,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
|
|
||||||
// POST /api/servers/test-connection — check credentials before saving
|
// POST /api/servers/test-connection — check credentials before saving
|
||||||
app.post("/test-connection", { schema: { body: TestConnectionSchema } }, async (req, reply) => {
|
app.post("/test-connection", { schema: { body: TestConnectionSchema } }, async (req, reply) => {
|
||||||
const { host, port, useSsl, verifySsl, apiPath, username, password } = req.body
|
const { host, port, useSsl, verifySsl, apiPath, username, password } = req.body as TestConnectionRequest
|
||||||
const client = new MikrotikClient({ host, port, useSsl, verifySsl, apiPath, username, password })
|
const client = new MikrotikClient({ host, port, useSsl, verifySsl, apiPath, username, password })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,17 +77,13 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
|
|
||||||
// GET /api/servers
|
// GET /api/servers
|
||||||
app.get("/", async (_req, reply) => {
|
app.get("/", async (_req, reply) => {
|
||||||
const all = db.select().from(servers).all()
|
return reply.send(listServersRead())
|
||||||
const result: ServerRead[] = all.map(s => toServerRead(s, getLatestSnapshot(s.id)))
|
|
||||||
return reply.send(result)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
|
// GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
|
||||||
app.get("/:id/ros-src-address", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.get("/:id/ros-src-address", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
const server = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const server = getServerRowById(params.id)
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
const ipv4 = await resolveRosSrcIpv4(server.host)
|
const ipv4 = await resolveRosSrcIpv4(server.host)
|
||||||
@@ -163,30 +92,15 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
|
|
||||||
// POST /api/servers
|
// POST /api/servers
|
||||||
app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
|
app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
|
||||||
const now = new Date().toISOString()
|
return reply.status(201).send(createServer(req.body as ServerCreateRequest))
|
||||||
const { wanUplinks, ...rest } = req.body
|
|
||||||
const [inserted] = db
|
|
||||||
.insert(servers)
|
|
||||||
.values({
|
|
||||||
...rest,
|
|
||||||
wanUplinks: JSON.stringify(wanUplinks ?? []),
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.all()
|
|
||||||
return reply.status(201).send(toServerRead(inserted, undefined))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET /api/servers/:id
|
// GET /api/servers/:id
|
||||||
app.get("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.get("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
const server = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const server = getServerReadById(params.id)
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
return reply.send(toServerRead(server, getLatestSnapshot(server.id)))
|
return reply.send(server)
|
||||||
})
|
})
|
||||||
|
|
||||||
// PUT /api/servers/:id
|
// PUT /api/servers/:id
|
||||||
@@ -194,58 +108,30 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
"/:id",
|
"/:id",
|
||||||
{ schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
|
{ schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
const existing = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const existing = getServerReadById(params.id)
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
return reply.send(updateServer(params.id, req.body as ServerUpdateRequest))
|
||||||
const body = req.body
|
|
||||||
const { wanUplinks, ...rest } = body
|
|
||||||
const setPayload: Record<string, unknown> = { updatedAt: new Date().toISOString() }
|
|
||||||
for (const [k, v] of Object.entries(rest)) {
|
|
||||||
if (v !== undefined) setPayload[k] = v
|
|
||||||
}
|
|
||||||
if (wanUplinks !== undefined) {
|
|
||||||
setPayload.wanUplinks = JSON.stringify(wanUplinks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updated] = db
|
|
||||||
.update(servers)
|
|
||||||
.set(setPayload as Partial<ServerRow>)
|
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.returning()
|
|
||||||
.all()
|
|
||||||
|
|
||||||
return reply.send(toServerRead(updated, getLatestSnapshot(updated.id)))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// DELETE /api/servers/:id
|
// DELETE /api/servers/:id
|
||||||
app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
const existing = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const existing = getServerReadById(params.id)
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
deleteServer(params.id)
|
||||||
db.delete(servers).where(eq(servers.id, req.params.id)).run()
|
|
||||||
return reply.status(204).send()
|
return reply.status(204).send()
|
||||||
})
|
})
|
||||||
|
|
||||||
// POST /api/servers/:id/poll
|
// POST /api/servers/:id/poll
|
||||||
app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
const existing = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const existing = getServerReadById(params.id)
|
||||||
.where(eq(servers.id, req.params.id))
|
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const snap = await pollServer(req.params.id)
|
const snap = await pollServer(params.id)
|
||||||
return reply.send(snap)
|
return reply.send(snap)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reply.status(500).send({ error: (err as Error).message })
|
return reply.status(500).send({ error: (err as Error).message })
|
||||||
@@ -257,22 +143,11 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
"/:id/snapshots",
|
"/:id/snapshots",
|
||||||
{ schema: { params: ServerIdParamSchema, querystring: SnapshotsQuerySchema } },
|
{ schema: { params: ServerIdParamSchema, querystring: SnapshotsQuerySchema } },
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
const existing = db
|
const params = req.params as ServerIdParams
|
||||||
.select().from(servers)
|
const query = req.query as SnapshotsQuery
|
||||||
.where(eq(servers.id, req.params.id))
|
const existing = getServerReadById(params.id)
|
||||||
.limit(1).all()[0]
|
|
||||||
|
|
||||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
return reply.send(listServerSnapshots(params.id, query.limit))
|
||||||
const snaps = db
|
|
||||||
.select()
|
|
||||||
.from(serverSnapshots)
|
|
||||||
.where(eq(serverSnapshots.serverId, req.params.id))
|
|
||||||
.orderBy(desc(serverSnapshots.polledAt))
|
|
||||||
.limit(req.query.limit)
|
|
||||||
.all()
|
|
||||||
|
|
||||||
return reply.send(snaps.map(toSnapshotRead))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-105
@@ -1,4 +1,16 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
serverCreateSchema,
|
||||||
|
serverIdParamSchema,
|
||||||
|
snapshotsQuerySchema,
|
||||||
|
serverUpdateSchema,
|
||||||
|
testConnectionSchema,
|
||||||
|
type ServerRead as ContractServerRead,
|
||||||
|
type SnapshotRead as ContractSnapshotRead,
|
||||||
|
type ServerCreate as ContractServerCreate,
|
||||||
|
type ServerUpdate as ContractServerUpdate,
|
||||||
|
type WanUplink as ContractWanUplinkRead,
|
||||||
|
} from "../../../packages/contracts/dist/servers.js"
|
||||||
|
|
||||||
// ── RouterOS raw response types ────────────────────────────────────────────────
|
// ── RouterOS raw response types ────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -49,116 +61,28 @@ export interface RosIpAddress {
|
|||||||
|
|
||||||
// ── Zod schemas for API validation ────────────────────────────────────────────
|
// ── Zod schemas for API validation ────────────────────────────────────────────
|
||||||
|
|
||||||
export const WanUplinkSchema = z.object({
|
export const ServerCreateSchema = serverCreateSchema
|
||||||
id: z.string(),
|
export const ServerUpdateSchema = serverUpdateSchema
|
||||||
name: z.string(),
|
export const TestConnectionSchema = testConnectionSchema
|
||||||
isp: z.string(),
|
export const ServerIdParamSchema = serverIdParamSchema
|
||||||
iface: z.string(),
|
export const SnapshotsQuerySchema = snapshotsQuerySchema
|
||||||
ip: z.string(),
|
|
||||||
maxDl: z.number(),
|
|
||||||
maxUl: z.number(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ServerCreateSchema = z.object({
|
// Request payload types for route handlers.
|
||||||
host: z.string().min(1, "Host is required"),
|
// (Type-provider-zod in this repo sometimes falls back to `unknown` during cross-package schema wiring.)
|
||||||
port: z.number().int().positive().default(443),
|
export type ServerIdParams = z.infer<typeof ServerIdParamSchema>
|
||||||
username: z.string().default("admin"),
|
export type TestConnectionRequest = z.infer<typeof TestConnectionSchema>
|
||||||
password: z.string().default(""),
|
export type SnapshotsQuery = z.infer<typeof SnapshotsQuerySchema>
|
||||||
useSsl: z.boolean().default(true),
|
export type ServerCreateRequest = z.infer<typeof ServerCreateSchema>
|
||||||
verifySsl: z.boolean().default(false),
|
export type ServerUpdateRequest = z.infer<typeof ServerUpdateSchema>
|
||||||
name: z.string().default(""),
|
|
||||||
type: z.enum(["jump-host", "exit-node", "home-router"]).default("home-router"),
|
|
||||||
site: z.string().default(""),
|
|
||||||
country: z.string().default(""),
|
|
||||||
asn: z.string().default(""),
|
|
||||||
comment: z.string().default(""),
|
|
||||||
enabled: z.boolean().default(true),
|
|
||||||
lanSubnet: z.string().default(""),
|
|
||||||
wanUplinks: z.array(WanUplinkSchema).default([]),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ServerUpdateSchema = ServerCreateSchema.partial().omit({ host: true }).extend({
|
|
||||||
host: z.string().min(1).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const TestConnectionSchema = z.object({
|
|
||||||
host: z.string().min(1),
|
|
||||||
port: z.number().int().positive().default(443),
|
|
||||||
useSsl: z.boolean().default(true),
|
|
||||||
verifySsl: z.boolean().default(false),
|
|
||||||
apiPath: z.string().default("/rest"),
|
|
||||||
username: z.string().default("admin"),
|
|
||||||
password: z.string().default(""),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ServerIdParamSchema = z.object({
|
|
||||||
id: z.coerce.number().int().positive(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const SnapshotsQuerySchema = z.object({
|
|
||||||
limit: z.coerce.number().int().positive().max(500).default(50),
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Response types (what the API returns) ─────────────────────────────────────
|
// ── Response types (what the API returns) ─────────────────────────────────────
|
||||||
|
|
||||||
/** Flat server response — mirrors the frontend's Server interface from lib/data.ts */
|
/** Flat server response — mirrors the frontend's Server interface from lib/data.ts */
|
||||||
export interface WanUplinkRead {
|
export type WanUplinkRead = ContractWanUplinkRead
|
||||||
id: string
|
export type ServerCreate = ContractServerCreate
|
||||||
name: string
|
export type ServerUpdate = ContractServerUpdate
|
||||||
isp: string
|
export type ServerRead = ContractServerRead
|
||||||
iface: string
|
export type SnapshotRead = ContractSnapshotRead
|
||||||
ip: string
|
|
||||||
maxDl: number
|
|
||||||
maxUl: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerRead {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
host: string
|
|
||||||
port: number
|
|
||||||
useSsl: boolean
|
|
||||||
verifySsl: boolean
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
type: "jump-host" | "exit-node" | "home-router"
|
|
||||||
site: string
|
|
||||||
country: string
|
|
||||||
asn: string
|
|
||||||
comment: string
|
|
||||||
enabled: boolean
|
|
||||||
lanSubnet: string
|
|
||||||
wanUplinks: WanUplinkRead[]
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
// from latest snapshot (null if never polled)
|
|
||||||
status: "online" | "offline" | null
|
|
||||||
latency: number | null // ms
|
|
||||||
os: string | null // RouterOS version
|
|
||||||
model: string | null // board-name
|
|
||||||
uptime: string | null
|
|
||||||
cpuLoad: number | null
|
|
||||||
freeMemory: number | null
|
|
||||||
totalMemory: number | null
|
|
||||||
identityName: string | null
|
|
||||||
sessions: number // always 0 for now
|
|
||||||
polledAt: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SnapshotRead {
|
|
||||||
id: number
|
|
||||||
serverId: number
|
|
||||||
polledAt: string
|
|
||||||
status: "online" | "offline"
|
|
||||||
latencyMs: number | null
|
|
||||||
rosVersion: string | null
|
|
||||||
boardName: string | null
|
|
||||||
uptime: string | null
|
|
||||||
cpuLoad: number | null
|
|
||||||
freeMemory: number | null
|
|
||||||
totalMemory: number | null
|
|
||||||
identityName: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── BGP types ────────────────────────────────────────────────────────────────
|
// ── BGP types ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "..",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Refactor Baseline
|
||||||
|
|
||||||
|
## Проверки
|
||||||
|
|
||||||
|
- Frontend lint: `npm run lint` (root) — failed (`20 errors`, `10 warnings`).
|
||||||
|
- Backend build: `npm run build` (`backend`) — passed.
|
||||||
|
- Backend smoke: `npm run test:alert-engine` (`backend`) — passed.
|
||||||
|
|
||||||
|
## Критичные baseline-проблемы lint
|
||||||
|
|
||||||
|
- Массовые `react-hooks/set-state-in-effect` в крупных UI-экранах и `components`.
|
||||||
|
- `prefer-const` ошибки в backend сервисах.
|
||||||
|
- Накопленные предупреждения `exhaustive-deps` и `no-unused-vars`.
|
||||||
|
|
||||||
|
## Архитектурные hotspots (для приоритетной миграции)
|
||||||
|
|
||||||
|
- Frontend giant pages:
|
||||||
|
- `app/(main)/alerts/page.tsx`
|
||||||
|
- `app/(main)/uptime/page.tsx`
|
||||||
|
- `app/(main)/network-map/page.tsx`
|
||||||
|
- `app/(main)/filters/page.tsx`
|
||||||
|
- `app/(main)/servers/page.tsx`
|
||||||
|
- Backend mixed layers:
|
||||||
|
- `backend/src/routes/servers.ts`
|
||||||
|
- `backend/src/routes/uptime.ts`
|
||||||
|
- `backend/src/routes/ospf.ts`
|
||||||
|
- `backend/src/services/alerts-service.ts`
|
||||||
|
|
||||||
|
## Дубли контрактов/типов
|
||||||
|
|
||||||
|
- `backend/src/types/scheduler-run-snapshot.ts` и `lib/scheduler-run-snapshot.ts`
|
||||||
|
- `backend/src/types/server.ts` и локальные `Backend*` типы страниц
|
||||||
|
|
||||||
|
## Инварианты рефакторинга
|
||||||
|
|
||||||
|
- Не менять API shape endpoint-ов.
|
||||||
|
- Не менять бизнес-логику.
|
||||||
|
- Не менять поведение UI.
|
||||||
|
- Переход через параллельный новый слой и поэтапное переключение.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ServerRead } from "@/packages/contracts/src/servers"
|
||||||
|
import type { Server } from "@/lib/data"
|
||||||
|
|
||||||
|
export function toFrontendServer(server: ServerRead): Server {
|
||||||
|
return {
|
||||||
|
id: String(server.id),
|
||||||
|
name: server.name || server.host,
|
||||||
|
host: server.host,
|
||||||
|
type: server.type,
|
||||||
|
site: server.site,
|
||||||
|
country: server.country,
|
||||||
|
asn: server.asn,
|
||||||
|
model: server.model ?? "—",
|
||||||
|
os: server.os ?? "—",
|
||||||
|
enabled: server.enabled,
|
||||||
|
status: server.status ?? "offline",
|
||||||
|
latency: server.latency != null ? Math.round(server.latency) : null,
|
||||||
|
sessions: server.sessions ?? 0,
|
||||||
|
comment: server.comment || undefined,
|
||||||
|
lanSubnet: server.lanSubnet || undefined,
|
||||||
|
wanUplinks: Array.isArray(server.wanUplinks) && server.wanUplinks.length ? server.wanUplinks : undefined,
|
||||||
|
uptime: server.uptime ?? undefined,
|
||||||
|
cpuLoad: server.cpuLoad ?? undefined,
|
||||||
|
freeMemory: server.freeMemory ?? undefined,
|
||||||
|
totalMemory: server.totalMemory ?? undefined,
|
||||||
|
polledAt: server.polledAt ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -388,7 +388,8 @@ export function buildLiveOptimizerData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bestAssign) {
|
if (bestAssign) {
|
||||||
for (const wan of home.wans) out.set(wan.id, bestAssign.get(wan.id))
|
const best: Map<string, RouteOptimizerSpeedProbe> = bestAssign as Map<string, RouteOptimizerSpeedProbe>
|
||||||
|
for (const wan of home.wans) out.set(wan.id, best.get(wan.id))
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export type AlertEngineRuleDiagSnapshot = {
|
|||||||
ruleId: string
|
ruleId: string
|
||||||
inGroup: boolean
|
inGroup: boolean
|
||||||
evalHit: boolean
|
evalHit: boolean
|
||||||
|
hitTransition?: "problem" | "recovery" | "neutral"
|
||||||
|
hitMessage?: string
|
||||||
stabilityOk: boolean
|
stabilityOk: boolean
|
||||||
cooldownOk: boolean
|
cooldownOk: boolean
|
||||||
telegramOk: boolean
|
telegramOk: boolean
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@mmapp/contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./servers": "./dist/servers.js",
|
||||||
|
"./alerts": "./dist/alerts.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+204
@@ -0,0 +1,204 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
export declare const alertTypeSchema: z.ZodEnum<{
|
||||||
|
"gre-tunnel": "gre-tunnel";
|
||||||
|
"bgp-peer": "bgp-peer";
|
||||||
|
"bgp-prefix": "bgp-prefix";
|
||||||
|
"gre-client": "gre-client";
|
||||||
|
server: "server";
|
||||||
|
rtt: "rtt";
|
||||||
|
loss: "loss";
|
||||||
|
traffic: "traffic";
|
||||||
|
}>;
|
||||||
|
export declare const alertSeveritySchema: z.ZodEnum<{
|
||||||
|
critical: "critical";
|
||||||
|
warning: "warning";
|
||||||
|
info: "info";
|
||||||
|
}>;
|
||||||
|
export declare const alertCooldownSchema: z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>;
|
||||||
|
export declare const recoveryModeSchema: z.ZodEnum<{
|
||||||
|
never: "never";
|
||||||
|
always: "always";
|
||||||
|
conditional: "conditional";
|
||||||
|
}>;
|
||||||
|
export declare const combineModeSchema: z.ZodEnum<{
|
||||||
|
any: "any";
|
||||||
|
all: "all";
|
||||||
|
}>;
|
||||||
|
export declare const alertGroupSchema: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
combineMode: z.ZodEnum<{
|
||||||
|
any: "any";
|
||||||
|
all: "all";
|
||||||
|
}>;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
cooldownOverride: z.ZodUnion<readonly [z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const alertRuleSchema: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
type: z.ZodEnum<{
|
||||||
|
"gre-tunnel": "gre-tunnel";
|
||||||
|
"bgp-peer": "bgp-peer";
|
||||||
|
"bgp-prefix": "bgp-prefix";
|
||||||
|
"gre-client": "gre-client";
|
||||||
|
server: "server";
|
||||||
|
rtt: "rtt";
|
||||||
|
loss: "loss";
|
||||||
|
traffic: "traffic";
|
||||||
|
}>;
|
||||||
|
target: z.ZodOptional<z.ZodString>;
|
||||||
|
targets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
groupId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
|
condition: z.ZodOptional<z.ZodString>;
|
||||||
|
conditions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
severity: z.ZodEnum<{
|
||||||
|
critical: "critical";
|
||||||
|
warning: "warning";
|
||||||
|
info: "info";
|
||||||
|
}>;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
cooldown: z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>;
|
||||||
|
confirmStabilitySec: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
|
||||||
|
recoveryMode: z.ZodOptional<z.ZodEnum<{
|
||||||
|
never: "never";
|
||||||
|
always: "always";
|
||||||
|
conditional: "conditional";
|
||||||
|
}>>;
|
||||||
|
recoveryStabilitySec: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
|
||||||
|
chatId: z.ZodString;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const putRulesSchema: z.ZodObject<{
|
||||||
|
rules: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
type: z.ZodEnum<{
|
||||||
|
"gre-tunnel": "gre-tunnel";
|
||||||
|
"bgp-peer": "bgp-peer";
|
||||||
|
"bgp-prefix": "bgp-prefix";
|
||||||
|
"gre-client": "gre-client";
|
||||||
|
server: "server";
|
||||||
|
rtt: "rtt";
|
||||||
|
loss: "loss";
|
||||||
|
traffic: "traffic";
|
||||||
|
}>;
|
||||||
|
target: z.ZodOptional<z.ZodString>;
|
||||||
|
targets: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
groupId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
|
condition: z.ZodOptional<z.ZodString>;
|
||||||
|
conditions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
severity: z.ZodEnum<{
|
||||||
|
critical: "critical";
|
||||||
|
warning: "warning";
|
||||||
|
info: "info";
|
||||||
|
}>;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
cooldown: z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>;
|
||||||
|
confirmStabilitySec: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
|
||||||
|
recoveryMode: z.ZodOptional<z.ZodEnum<{
|
||||||
|
never: "never";
|
||||||
|
always: "always";
|
||||||
|
conditional: "conditional";
|
||||||
|
}>>;
|
||||||
|
recoveryStabilitySec: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
|
||||||
|
chatId: z.ZodString;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
groups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
combineMode: z.ZodEnum<{
|
||||||
|
any: "any";
|
||||||
|
all: "all";
|
||||||
|
}>;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
cooldownOverride: z.ZodUnion<readonly [z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const putTelegramSchema: z.ZodObject<{
|
||||||
|
token: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
||||||
|
chatId: z.ZodOptional<z.ZodString>;
|
||||||
|
messageThreadId: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const rulePreviewTelegramSchema: z.ZodObject<{
|
||||||
|
name: z.ZodString;
|
||||||
|
targets: z.ZodArray<z.ZodString>;
|
||||||
|
conditionLine: z.ZodString;
|
||||||
|
severity: z.ZodEnum<{
|
||||||
|
critical: "critical";
|
||||||
|
warning: "warning";
|
||||||
|
info: "info";
|
||||||
|
}>;
|
||||||
|
cooldown: z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const testTelegramSchema: z.ZodObject<{
|
||||||
|
token: z.ZodOptional<z.ZodString>;
|
||||||
|
chatId: z.ZodOptional<z.ZodString>;
|
||||||
|
messageThreadId: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
||||||
|
rulePreview: z.ZodOptional<z.ZodObject<{
|
||||||
|
name: z.ZodString;
|
||||||
|
targets: z.ZodArray<z.ZodString>;
|
||||||
|
conditionLine: z.ZodString;
|
||||||
|
severity: z.ZodEnum<{
|
||||||
|
critical: "critical";
|
||||||
|
warning: "warning";
|
||||||
|
info: "info";
|
||||||
|
}>;
|
||||||
|
cooldown: z.ZodEnum<{
|
||||||
|
"1\u043C": "1м";
|
||||||
|
"5\u043C": "5м";
|
||||||
|
"15\u043C": "15м";
|
||||||
|
"1\u0447": "1ч";
|
||||||
|
"4\u0447": "4ч";
|
||||||
|
"24\u0447": "24ч";
|
||||||
|
}>;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export type AlertGroup = z.infer<typeof alertGroupSchema>;
|
||||||
|
export type AlertRule = z.infer<typeof alertRuleSchema>;
|
||||||
|
export type PutRulesBody = z.infer<typeof putRulesSchema>;
|
||||||
|
export type PutTelegramBody = z.infer<typeof putTelegramSchema>;
|
||||||
|
export type RulePreviewTelegram = z.infer<typeof rulePreviewTelegramSchema>;
|
||||||
|
export type TestTelegramBody = z.infer<typeof testTelegramSchema>;
|
||||||
|
//# sourceMappingURL=alerts.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"alerts.d.ts","sourceRoot":"","sources":["alerts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,eAAe;;;;;;;;;EAS1B,CAAA;AACF,eAAO,MAAM,mBAAmB;;;;EAA0C,CAAA;AAC1E,eAAO,MAAM,mBAAmB;;;;;;;EAAiD,CAAA;AACjF,eAAO,MAAM,kBAAkB;;;;EAA6C,CAAA;AAC5E,eAAO,MAAM,iBAAiB;;;EAAyB,CAAA;AAEvD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;iBAM3B,CAAA;AAEF,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyBxB,CAAA;AAEJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAGzB,CAAA;AAEF,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAA;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;iBAMpC,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;iBAK7B,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAA;AACzD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAC/D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAC3E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
export const alertTypeSchema = z.enum([
|
||||||
|
"gre-tunnel",
|
||||||
|
"bgp-peer",
|
||||||
|
"bgp-prefix",
|
||||||
|
"gre-client",
|
||||||
|
"server",
|
||||||
|
"rtt",
|
||||||
|
"loss",
|
||||||
|
"traffic",
|
||||||
|
]);
|
||||||
|
export const alertSeveritySchema = z.enum(["critical", "warning", "info"]);
|
||||||
|
export const alertCooldownSchema = z.enum(["1м", "5м", "15м", "1ч", "4ч", "24ч"]);
|
||||||
|
export const recoveryModeSchema = z.enum(["always", "never", "conditional"]);
|
||||||
|
export const combineModeSchema = z.enum(["any", "all"]);
|
||||||
|
export const alertGroupSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
combineMode: combineModeSchema,
|
||||||
|
enabled: z.boolean(),
|
||||||
|
cooldownOverride: z.union([alertCooldownSchema, z.null()]),
|
||||||
|
});
|
||||||
|
export const alertRuleSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
type: alertTypeSchema,
|
||||||
|
target: z.string().optional(),
|
||||||
|
targets: z.array(z.string().min(1)).optional(),
|
||||||
|
groupId: z.string().nullable().optional(),
|
||||||
|
condition: z.string().optional(),
|
||||||
|
conditions: z.array(z.string().min(1)).optional(),
|
||||||
|
severity: alertSeveritySchema,
|
||||||
|
enabled: z.boolean(),
|
||||||
|
cooldown: alertCooldownSchema,
|
||||||
|
confirmStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
||||||
|
recoveryMode: recoveryModeSchema.optional(),
|
||||||
|
recoveryStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
||||||
|
chatId: z.string(),
|
||||||
|
})
|
||||||
|
.refine((r) => (r.targets != null && r.targets.length > 0) || Boolean(r.target?.trim()), {
|
||||||
|
message: "Нужен хотя бы один объект: targets или target",
|
||||||
|
path: ["targets"],
|
||||||
|
})
|
||||||
|
.refine((r) => (r.conditions != null && r.conditions.length > 0) || Boolean(r.condition?.trim()), {
|
||||||
|
message: "Нужно хотя бы одно условие: conditions или condition",
|
||||||
|
path: ["conditions"],
|
||||||
|
});
|
||||||
|
export const putRulesSchema = z.object({
|
||||||
|
rules: z.array(alertRuleSchema),
|
||||||
|
groups: z.array(alertGroupSchema).optional(),
|
||||||
|
});
|
||||||
|
export const putTelegramSchema = z.object({
|
||||||
|
token: z.union([z.string(), z.null()]).optional(),
|
||||||
|
chatId: z.string().optional(),
|
||||||
|
messageThreadId: z.union([z.number().int().positive(), z.null()]).optional(),
|
||||||
|
});
|
||||||
|
export const rulePreviewTelegramSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
targets: z.array(z.string().min(1)).min(1),
|
||||||
|
conditionLine: z.string().min(1),
|
||||||
|
severity: alertSeveritySchema,
|
||||||
|
cooldown: alertCooldownSchema,
|
||||||
|
});
|
||||||
|
export const testTelegramSchema = z.object({
|
||||||
|
token: z.string().optional(),
|
||||||
|
chatId: z.string().optional(),
|
||||||
|
messageThreadId: z.coerce.number().int().positive().optional(),
|
||||||
|
rulePreview: rulePreviewTelegramSchema.optional(),
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=alerts.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"alerts.js","sourceRoot":"","sources":["alerts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC;IACpC,YAAY;IACZ,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,KAAK;IACL,MAAM;IACN,SAAS;CACV,CAAC,CAAA;AACF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAA;AAC1E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAA;AAC5E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAEvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,WAAW,EAAE,iBAAiB;IAC9B,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,gBAAgB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,mBAAmB,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;CAC3D,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,eAAe;IACrB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC9C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACzC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACjD,QAAQ,EAAE,mBAAmB;IAC7B,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,QAAQ,EAAE,mBAAmB;IAC7B,mBAAmB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IACvF,YAAY,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IAC3C,oBAAoB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE;IACvF,OAAO,EAAE,+CAA+C;IACxD,IAAI,EAAE,CAAC,SAAS,CAAC;CAClB,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE;IAChG,OAAO,EAAE,sDAAsD;IAC/D,IAAI,EAAE,CAAC,YAAY,CAAC;CACrB,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC;IAC/B,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IACjD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC7E,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,mBAAmB;CAC9B,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC9D,WAAW,EAAE,yBAAyB,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAA"}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const alertTypeSchema = z.enum([
|
||||||
|
"gre-tunnel",
|
||||||
|
"bgp-peer",
|
||||||
|
"bgp-prefix",
|
||||||
|
"gre-client",
|
||||||
|
"server",
|
||||||
|
"rtt",
|
||||||
|
"loss",
|
||||||
|
"traffic",
|
||||||
|
])
|
||||||
|
export const alertSeveritySchema = z.enum(["critical", "warning", "info"])
|
||||||
|
export const alertCooldownSchema = z.enum(["1м", "5м", "15м", "1ч", "4ч", "24ч"])
|
||||||
|
export const recoveryModeSchema = z.enum(["always", "never", "conditional"])
|
||||||
|
export const combineModeSchema = z.enum(["any", "all"])
|
||||||
|
|
||||||
|
export const alertGroupSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
combineMode: combineModeSchema,
|
||||||
|
enabled: z.boolean(),
|
||||||
|
cooldownOverride: z.union([alertCooldownSchema, z.null()]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const alertRuleSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
type: alertTypeSchema,
|
||||||
|
target: z.string().optional(),
|
||||||
|
targets: z.array(z.string().min(1)).optional(),
|
||||||
|
groupId: z.string().nullable().optional(),
|
||||||
|
condition: z.string().optional(),
|
||||||
|
conditions: z.array(z.string().min(1)).optional(),
|
||||||
|
severity: alertSeveritySchema,
|
||||||
|
enabled: z.boolean(),
|
||||||
|
cooldown: alertCooldownSchema,
|
||||||
|
confirmStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
||||||
|
recoveryMode: recoveryModeSchema.optional(),
|
||||||
|
recoveryStabilitySec: z.union([z.number().int().min(0).max(86400), z.null()]).optional(),
|
||||||
|
chatId: z.string(),
|
||||||
|
})
|
||||||
|
.refine((r) => (r.targets != null && r.targets.length > 0) || Boolean(r.target?.trim()), {
|
||||||
|
message: "Нужен хотя бы один объект: targets или target",
|
||||||
|
path: ["targets"],
|
||||||
|
})
|
||||||
|
.refine((r) => (r.conditions != null && r.conditions.length > 0) || Boolean(r.condition?.trim()), {
|
||||||
|
message: "Нужно хотя бы одно условие: conditions или condition",
|
||||||
|
path: ["conditions"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const putRulesSchema = z.object({
|
||||||
|
rules: z.array(alertRuleSchema),
|
||||||
|
groups: z.array(alertGroupSchema).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const putTelegramSchema = z.object({
|
||||||
|
token: z.union([z.string(), z.null()]).optional(),
|
||||||
|
chatId: z.string().optional(),
|
||||||
|
messageThreadId: z.union([z.number().int().positive(), z.null()]).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const rulePreviewTelegramSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
targets: z.array(z.string().min(1)).min(1),
|
||||||
|
conditionLine: z.string().min(1),
|
||||||
|
severity: alertSeveritySchema,
|
||||||
|
cooldown: alertCooldownSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const testTelegramSchema = z.object({
|
||||||
|
token: z.string().optional(),
|
||||||
|
chatId: z.string().optional(),
|
||||||
|
messageThreadId: z.coerce.number().int().positive().optional(),
|
||||||
|
rulePreview: rulePreviewTelegramSchema.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AlertGroup = z.infer<typeof alertGroupSchema>
|
||||||
|
export type AlertRule = z.infer<typeof alertRuleSchema>
|
||||||
|
export type PutRulesBody = z.infer<typeof putRulesSchema>
|
||||||
|
export type PutTelegramBody = z.infer<typeof putTelegramSchema>
|
||||||
|
export type RulePreviewTelegram = z.infer<typeof rulePreviewTelegramSchema>
|
||||||
|
export type TestTelegramBody = z.infer<typeof testTelegramSchema>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./servers.js"
|
||||||
|
export * from "./alerts.js"
|
||||||
Vendored
+228
@@ -0,0 +1,228 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
export declare const serverTypeSchema: z.ZodEnum<{
|
||||||
|
"jump-host": "jump-host";
|
||||||
|
"exit-node": "exit-node";
|
||||||
|
"home-router": "home-router";
|
||||||
|
}>;
|
||||||
|
export declare const serverStatusSchema: z.ZodEnum<{
|
||||||
|
online: "online";
|
||||||
|
offline: "offline";
|
||||||
|
}>;
|
||||||
|
export declare const wanUplinkSchema: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
isp: z.ZodString;
|
||||||
|
iface: z.ZodString;
|
||||||
|
ip: z.ZodString;
|
||||||
|
maxDl: z.ZodNumber;
|
||||||
|
maxUl: z.ZodNumber;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const serverCreateSchema: z.ZodObject<{
|
||||||
|
host: z.ZodString;
|
||||||
|
port: z.ZodDefault<z.ZodNumber>;
|
||||||
|
username: z.ZodDefault<z.ZodString>;
|
||||||
|
password: z.ZodDefault<z.ZodString>;
|
||||||
|
useSsl: z.ZodDefault<z.ZodBoolean>;
|
||||||
|
verifySsl: z.ZodDefault<z.ZodBoolean>;
|
||||||
|
name: z.ZodDefault<z.ZodString>;
|
||||||
|
type: z.ZodDefault<z.ZodEnum<{
|
||||||
|
"jump-host": "jump-host";
|
||||||
|
"exit-node": "exit-node";
|
||||||
|
"home-router": "home-router";
|
||||||
|
}>>;
|
||||||
|
site: z.ZodDefault<z.ZodString>;
|
||||||
|
country: z.ZodDefault<z.ZodString>;
|
||||||
|
asn: z.ZodDefault<z.ZodString>;
|
||||||
|
comment: z.ZodDefault<z.ZodString>;
|
||||||
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||||
|
lanSubnet: z.ZodDefault<z.ZodString>;
|
||||||
|
wanUplinks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
isp: z.ZodString;
|
||||||
|
iface: z.ZodString;
|
||||||
|
ip: z.ZodString;
|
||||||
|
maxDl: z.ZodNumber;
|
||||||
|
maxUl: z.ZodNumber;
|
||||||
|
}, z.core.$strip>>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const serverUpdateSchema: z.ZodObject<{
|
||||||
|
type: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
||||||
|
"jump-host": "jump-host";
|
||||||
|
"exit-node": "exit-node";
|
||||||
|
"home-router": "home-router";
|
||||||
|
}>>>;
|
||||||
|
name: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
port: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
||||||
|
username: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
password: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
useSsl: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
||||||
|
verifySsl: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
||||||
|
site: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
country: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
asn: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
comment: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
enabled: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
||||||
|
lanSubnet: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||||
|
wanUplinks: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
isp: z.ZodString;
|
||||||
|
iface: z.ZodString;
|
||||||
|
ip: z.ZodString;
|
||||||
|
maxDl: z.ZodNumber;
|
||||||
|
maxUl: z.ZodNumber;
|
||||||
|
}, z.core.$strip>>>>;
|
||||||
|
host: z.ZodOptional<z.ZodString>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const serverIdParamSchema: z.ZodObject<{
|
||||||
|
id: z.ZodCoercedNumber<unknown>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const snapshotsQuerySchema: z.ZodObject<{
|
||||||
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const testConnectionSchema: z.ZodObject<{
|
||||||
|
host: z.ZodString;
|
||||||
|
port: z.ZodDefault<z.ZodNumber>;
|
||||||
|
useSsl: z.ZodDefault<z.ZodBoolean>;
|
||||||
|
verifySsl: z.ZodDefault<z.ZodBoolean>;
|
||||||
|
apiPath: z.ZodDefault<z.ZodString>;
|
||||||
|
username: z.ZodDefault<z.ZodString>;
|
||||||
|
password: z.ZodDefault<z.ZodString>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const serverReadSchema: z.ZodObject<{
|
||||||
|
id: z.ZodNumber;
|
||||||
|
name: z.ZodString;
|
||||||
|
host: z.ZodString;
|
||||||
|
port: z.ZodNumber;
|
||||||
|
useSsl: z.ZodBoolean;
|
||||||
|
verifySsl: z.ZodBoolean;
|
||||||
|
username: z.ZodString;
|
||||||
|
password: z.ZodString;
|
||||||
|
type: z.ZodEnum<{
|
||||||
|
"jump-host": "jump-host";
|
||||||
|
"exit-node": "exit-node";
|
||||||
|
"home-router": "home-router";
|
||||||
|
}>;
|
||||||
|
site: z.ZodString;
|
||||||
|
country: z.ZodString;
|
||||||
|
asn: z.ZodString;
|
||||||
|
comment: z.ZodString;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
lanSubnet: z.ZodString;
|
||||||
|
wanUplinks: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
isp: z.ZodString;
|
||||||
|
iface: z.ZodString;
|
||||||
|
ip: z.ZodString;
|
||||||
|
maxDl: z.ZodNumber;
|
||||||
|
maxUl: z.ZodNumber;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
createdAt: z.ZodString;
|
||||||
|
updatedAt: z.ZodString;
|
||||||
|
status: z.ZodUnion<readonly [z.ZodEnum<{
|
||||||
|
online: "online";
|
||||||
|
offline: "offline";
|
||||||
|
}>, z.ZodNull]>;
|
||||||
|
latency: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
os: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
model: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
uptime: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
cpuLoad: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
freeMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
totalMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
identityName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
sessions: z.ZodNumber;
|
||||||
|
polledAt: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const snapshotReadSchema: z.ZodObject<{
|
||||||
|
id: z.ZodNumber;
|
||||||
|
serverId: z.ZodNumber;
|
||||||
|
polledAt: z.ZodString;
|
||||||
|
status: z.ZodEnum<{
|
||||||
|
online: "online";
|
||||||
|
offline: "offline";
|
||||||
|
}>;
|
||||||
|
latencyMs: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
rosVersion: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
boardName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
uptime: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
cpuLoad: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
freeMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
totalMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
identityName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
export declare const serverListSchema: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodNumber;
|
||||||
|
name: z.ZodString;
|
||||||
|
host: z.ZodString;
|
||||||
|
port: z.ZodNumber;
|
||||||
|
useSsl: z.ZodBoolean;
|
||||||
|
verifySsl: z.ZodBoolean;
|
||||||
|
username: z.ZodString;
|
||||||
|
password: z.ZodString;
|
||||||
|
type: z.ZodEnum<{
|
||||||
|
"jump-host": "jump-host";
|
||||||
|
"exit-node": "exit-node";
|
||||||
|
"home-router": "home-router";
|
||||||
|
}>;
|
||||||
|
site: z.ZodString;
|
||||||
|
country: z.ZodString;
|
||||||
|
asn: z.ZodString;
|
||||||
|
comment: z.ZodString;
|
||||||
|
enabled: z.ZodBoolean;
|
||||||
|
lanSubnet: z.ZodString;
|
||||||
|
wanUplinks: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
isp: z.ZodString;
|
||||||
|
iface: z.ZodString;
|
||||||
|
ip: z.ZodString;
|
||||||
|
maxDl: z.ZodNumber;
|
||||||
|
maxUl: z.ZodNumber;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
createdAt: z.ZodString;
|
||||||
|
updatedAt: z.ZodString;
|
||||||
|
status: z.ZodUnion<readonly [z.ZodEnum<{
|
||||||
|
online: "online";
|
||||||
|
offline: "offline";
|
||||||
|
}>, z.ZodNull]>;
|
||||||
|
latency: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
os: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
model: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
uptime: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
cpuLoad: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
freeMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
totalMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
identityName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
sessions: z.ZodNumber;
|
||||||
|
polledAt: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
export declare const snapshotListSchema: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodNumber;
|
||||||
|
serverId: z.ZodNumber;
|
||||||
|
polledAt: z.ZodString;
|
||||||
|
status: z.ZodEnum<{
|
||||||
|
online: "online";
|
||||||
|
offline: "offline";
|
||||||
|
}>;
|
||||||
|
latencyMs: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
rosVersion: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
boardName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
uptime: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
cpuLoad: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
freeMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
totalMemory: z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>;
|
||||||
|
identityName: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
export type ServerType = z.infer<typeof serverTypeSchema>;
|
||||||
|
export type WanUplink = z.infer<typeof wanUplinkSchema>;
|
||||||
|
export type ServerCreate = z.infer<typeof serverCreateSchema>;
|
||||||
|
export type ServerUpdate = z.infer<typeof serverUpdateSchema>;
|
||||||
|
export type ServerRead = z.infer<typeof serverReadSchema>;
|
||||||
|
export type SnapshotRead = z.infer<typeof snapshotReadSchema>;
|
||||||
|
export type ServerIdParam = z.infer<typeof serverIdParamSchema>;
|
||||||
|
export type SnapshotsQuery = z.infer<typeof snapshotsQuerySchema>;
|
||||||
|
export type TestConnectionRequest = z.infer<typeof testConnectionSchema>;
|
||||||
|
//# sourceMappingURL=servers.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["servers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,gBAAgB;;;;EAAoD,CAAA;AACjF,eAAO,MAAM,kBAAkB;;;EAAgC,CAAA;AAE/D,eAAO,MAAM,eAAe;;;;;;;;iBAQ1B,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgB7B,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAE7B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;iBAE/B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;iBAQ/B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8B3B,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBAa7B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAA;AACzD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;kBAA8B,CAAA;AAE7D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA"}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
export const serverTypeSchema = z.enum(["jump-host", "exit-node", "home-router"]);
|
||||||
|
export const serverStatusSchema = z.enum(["online", "offline"]);
|
||||||
|
export const wanUplinkSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
isp: z.string(),
|
||||||
|
iface: z.string(),
|
||||||
|
ip: z.string(),
|
||||||
|
maxDl: z.number(),
|
||||||
|
maxUl: z.number(),
|
||||||
|
});
|
||||||
|
export const serverCreateSchema = z.object({
|
||||||
|
host: z.string().min(1, "Host is required"),
|
||||||
|
port: z.number().int().positive().default(443),
|
||||||
|
username: z.string().default("admin"),
|
||||||
|
password: z.string().default(""),
|
||||||
|
useSsl: z.boolean().default(true),
|
||||||
|
verifySsl: z.boolean().default(false),
|
||||||
|
name: z.string().default(""),
|
||||||
|
type: serverTypeSchema.default("home-router"),
|
||||||
|
site: z.string().default(""),
|
||||||
|
country: z.string().default(""),
|
||||||
|
asn: z.string().default(""),
|
||||||
|
comment: z.string().default(""),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
lanSubnet: z.string().default(""),
|
||||||
|
wanUplinks: z.array(wanUplinkSchema).default([]),
|
||||||
|
});
|
||||||
|
export const serverUpdateSchema = serverCreateSchema.partial().omit({ host: true }).extend({
|
||||||
|
host: z.string().min(1).optional(),
|
||||||
|
});
|
||||||
|
export const serverIdParamSchema = z.object({
|
||||||
|
id: z.coerce.number().int().positive(),
|
||||||
|
});
|
||||||
|
export const snapshotsQuerySchema = z.object({
|
||||||
|
limit: z.coerce.number().int().positive().max(500).default(50),
|
||||||
|
});
|
||||||
|
export const testConnectionSchema = z.object({
|
||||||
|
host: z.string().min(1),
|
||||||
|
port: z.number().int().positive().default(443),
|
||||||
|
useSsl: z.boolean().default(true),
|
||||||
|
verifySsl: z.boolean().default(false),
|
||||||
|
apiPath: z.string().default("/rest"),
|
||||||
|
username: z.string().default("admin"),
|
||||||
|
password: z.string().default(""),
|
||||||
|
});
|
||||||
|
export const serverReadSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
name: z.string(),
|
||||||
|
host: z.string(),
|
||||||
|
port: z.number().int(),
|
||||||
|
useSsl: z.boolean(),
|
||||||
|
verifySsl: z.boolean(),
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
type: serverTypeSchema,
|
||||||
|
site: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
asn: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
lanSubnet: z.string(),
|
||||||
|
wanUplinks: z.array(wanUplinkSchema),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
status: z.union([serverStatusSchema, z.null()]),
|
||||||
|
latency: z.union([z.number(), z.null()]),
|
||||||
|
os: z.union([z.string(), z.null()]),
|
||||||
|
model: z.union([z.string(), z.null()]),
|
||||||
|
uptime: z.union([z.string(), z.null()]),
|
||||||
|
cpuLoad: z.union([z.number(), z.null()]),
|
||||||
|
freeMemory: z.union([z.number(), z.null()]),
|
||||||
|
totalMemory: z.union([z.number(), z.null()]),
|
||||||
|
identityName: z.union([z.string(), z.null()]),
|
||||||
|
sessions: z.number().int(),
|
||||||
|
polledAt: z.union([z.string(), z.null()]),
|
||||||
|
});
|
||||||
|
export const snapshotReadSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
serverId: z.number().int(),
|
||||||
|
polledAt: z.string(),
|
||||||
|
status: serverStatusSchema,
|
||||||
|
latencyMs: z.union([z.number(), z.null()]),
|
||||||
|
rosVersion: z.union([z.string(), z.null()]),
|
||||||
|
boardName: z.union([z.string(), z.null()]),
|
||||||
|
uptime: z.union([z.string(), z.null()]),
|
||||||
|
cpuLoad: z.union([z.number().int(), z.null()]),
|
||||||
|
freeMemory: z.union([z.number().int(), z.null()]),
|
||||||
|
totalMemory: z.union([z.number().int(), z.null()]),
|
||||||
|
identityName: z.union([z.string(), z.null()]),
|
||||||
|
});
|
||||||
|
export const serverListSchema = z.array(serverReadSchema);
|
||||||
|
export const snapshotListSchema = z.array(snapshotReadSchema);
|
||||||
|
//# sourceMappingURL=servers.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const serverTypeSchema = z.enum(["jump-host", "exit-node", "home-router"])
|
||||||
|
export const serverStatusSchema = z.enum(["online", "offline"])
|
||||||
|
|
||||||
|
export const wanUplinkSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
isp: z.string(),
|
||||||
|
iface: z.string(),
|
||||||
|
ip: z.string(),
|
||||||
|
maxDl: z.number(),
|
||||||
|
maxUl: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverCreateSchema = z.object({
|
||||||
|
host: z.string().min(1, "Host is required"),
|
||||||
|
port: z.number().int().positive().default(443),
|
||||||
|
username: z.string().default("admin"),
|
||||||
|
password: z.string().default(""),
|
||||||
|
useSsl: z.boolean().default(true),
|
||||||
|
verifySsl: z.boolean().default(false),
|
||||||
|
name: z.string().default(""),
|
||||||
|
type: serverTypeSchema.default("home-router"),
|
||||||
|
site: z.string().default(""),
|
||||||
|
country: z.string().default(""),
|
||||||
|
asn: z.string().default(""),
|
||||||
|
comment: z.string().default(""),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
lanSubnet: z.string().default(""),
|
||||||
|
wanUplinks: z.array(wanUplinkSchema).default([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverUpdateSchema = serverCreateSchema.partial().omit({ host: true }).extend({
|
||||||
|
host: z.string().min(1).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverIdParamSchema = z.object({
|
||||||
|
id: z.coerce.number().int().positive(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const snapshotsQuerySchema = z.object({
|
||||||
|
limit: z.coerce.number().int().positive().max(500).default(50),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const testConnectionSchema = z.object({
|
||||||
|
host: z.string().min(1),
|
||||||
|
port: z.number().int().positive().default(443),
|
||||||
|
useSsl: z.boolean().default(true),
|
||||||
|
verifySsl: z.boolean().default(false),
|
||||||
|
apiPath: z.string().default("/rest"),
|
||||||
|
username: z.string().default("admin"),
|
||||||
|
password: z.string().default(""),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverReadSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
name: z.string(),
|
||||||
|
host: z.string(),
|
||||||
|
port: z.number().int(),
|
||||||
|
useSsl: z.boolean(),
|
||||||
|
verifySsl: z.boolean(),
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
type: serverTypeSchema,
|
||||||
|
site: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
asn: z.string(),
|
||||||
|
comment: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
lanSubnet: z.string(),
|
||||||
|
wanUplinks: z.array(wanUplinkSchema),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
status: z.union([serverStatusSchema, z.null()]),
|
||||||
|
latency: z.union([z.number(), z.null()]),
|
||||||
|
os: z.union([z.string(), z.null()]),
|
||||||
|
model: z.union([z.string(), z.null()]),
|
||||||
|
uptime: z.union([z.string(), z.null()]),
|
||||||
|
cpuLoad: z.union([z.number(), z.null()]),
|
||||||
|
freeMemory: z.union([z.number(), z.null()]),
|
||||||
|
totalMemory: z.union([z.number(), z.null()]),
|
||||||
|
identityName: z.union([z.string(), z.null()]),
|
||||||
|
sessions: z.number().int(),
|
||||||
|
polledAt: z.union([z.string(), z.null()]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const snapshotReadSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
serverId: z.number().int(),
|
||||||
|
polledAt: z.string(),
|
||||||
|
status: serverStatusSchema,
|
||||||
|
latencyMs: z.union([z.number(), z.null()]),
|
||||||
|
rosVersion: z.union([z.string(), z.null()]),
|
||||||
|
boardName: z.union([z.string(), z.null()]),
|
||||||
|
uptime: z.union([z.string(), z.null()]),
|
||||||
|
cpuLoad: z.union([z.number().int(), z.null()]),
|
||||||
|
freeMemory: z.union([z.number().int(), z.null()]),
|
||||||
|
totalMemory: z.union([z.number().int(), z.null()]),
|
||||||
|
identityName: z.union([z.string(), z.null()]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const serverListSchema = z.array(serverReadSchema)
|
||||||
|
export const snapshotListSchema = z.array(snapshotReadSchema)
|
||||||
|
|
||||||
|
export type ServerType = z.infer<typeof serverTypeSchema>
|
||||||
|
export type WanUplink = z.infer<typeof wanUplinkSchema>
|
||||||
|
export type ServerCreate = z.infer<typeof serverCreateSchema>
|
||||||
|
export type ServerUpdate = z.infer<typeof serverUpdateSchema>
|
||||||
|
export type ServerRead = z.infer<typeof serverReadSchema>
|
||||||
|
export type SnapshotRead = z.infer<typeof snapshotReadSchema>
|
||||||
|
export type ServerIdParam = z.infer<typeof serverIdParamSchema>
|
||||||
|
export type SnapshotsQuery = z.infer<typeof snapshotsQuerySchema>
|
||||||
|
export type TestConnectionRequest = z.infer<typeof testConnectionSchema>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export class ApiClientError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly status: number,
|
||||||
|
public readonly payload?: unknown,
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = "ApiClientError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimBaseUrl(baseUrl: string): string {
|
||||||
|
return baseUrl.replace(/\/$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...(hasBody ? { "Content-Type": "application/json" } : {}),
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 204) return undefined as T
|
||||||
|
|
||||||
|
const payload = await res.json().catch(() => undefined)
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg =
|
||||||
|
typeof payload === "object" &&
|
||||||
|
payload !== null &&
|
||||||
|
"error" in payload &&
|
||||||
|
typeof (payload as { error?: unknown }).error === "string"
|
||||||
|
? (payload as { error: string }).error
|
||||||
|
: res.statusText
|
||||||
|
throw new ApiClientError(msg, res.status, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as T
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
serverListSchema,
|
||||||
|
serverReadSchema,
|
||||||
|
testConnectionSchema,
|
||||||
|
type ServerCreate,
|
||||||
|
type ServerRead,
|
||||||
|
type ServerUpdate,
|
||||||
|
type TestConnectionRequest,
|
||||||
|
} from "@/packages/contracts/src/servers"
|
||||||
|
import { requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
|
type TestConnectionResponse = {
|
||||||
|
success: boolean
|
||||||
|
latencyMs?: number
|
||||||
|
identity?: string
|
||||||
|
version?: string
|
||||||
|
boardName?: string
|
||||||
|
uptime?: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listServers(baseUrl: string): Promise<ServerRead[]> {
|
||||||
|
const payload = await requestJson<unknown>(baseUrl, "/api/servers")
|
||||||
|
return serverListSchema.parse(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServer(baseUrl: string, id: string): Promise<ServerRead> {
|
||||||
|
const payload = await requestJson<unknown>(baseUrl, `/api/servers/${id}`)
|
||||||
|
return serverReadSchema.parse(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createServer(baseUrl: string, data: ServerCreate): Promise<ServerRead> {
|
||||||
|
const payload = await requestJson<unknown>(baseUrl, "/api/servers", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
return serverReadSchema.parse(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateServer(baseUrl: string, id: string, data: ServerUpdate): Promise<ServerRead> {
|
||||||
|
const payload = await requestJson<unknown>(baseUrl, `/api/servers/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
return serverReadSchema.parse(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteServer(baseUrl: string, id: string): Promise<void> {
|
||||||
|
await requestJson<void>(baseUrl, `/api/servers/${id}`, { method: "DELETE" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pollServer(baseUrl: string, id: string): Promise<void> {
|
||||||
|
await requestJson<unknown>(baseUrl, `/api/servers/${id}/poll`, { method: "POST" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testServerConnection(
|
||||||
|
baseUrl: string,
|
||||||
|
payload: TestConnectionRequest,
|
||||||
|
): Promise<TestConnectionResponse> {
|
||||||
|
testConnectionSchema.parse(payload)
|
||||||
|
return requestJson<TestConnectionResponse>(baseUrl, "/api/servers/test-connection", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user