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"
|
||||
import { Flag, countryName } from "@/components/flag"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -165,17 +166,7 @@ interface BgpSessionListRow {
|
||||
|
||||
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)
|
||||
}
|
||||
if (res.status === 204) return undefined as T
|
||||
return res.json() as Promise<T>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,27 +24,12 @@ import { Flag } from "@/components/flag"
|
||||
import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, FilterIcon, DownloadIcon } from "lucide-react"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type SchedulerRunRowDto,
|
||||
type UptimeSettingsDto,
|
||||
} from "@/lib/scheduler-settings"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
parseSchedulerRunSnapshot,
|
||||
type AlertEngineRuleDiagSnapshot,
|
||||
@@ -49,23 +50,7 @@ import {
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
|
||||
type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
|
||||
@@ -1491,16 +1492,7 @@ function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset
|
||||
|
||||
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 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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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 type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhGroup, IkeVersion, Server } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -210,16 +211,7 @@ interface GreTunnelsApiResponse {
|
||||
|
||||
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 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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type ServerType,
|
||||
} from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
buildGreMapEdges,
|
||||
buildServerResourceMap,
|
||||
@@ -175,16 +176,7 @@ function apiGreToGreTunnel(t: ApiGreTunnelRow): GreTunnel {
|
||||
|
||||
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)
|
||||
}
|
||||
return res.json() as Promise<T>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LoaderCircleIcon, AlertCircleIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -66,16 +67,7 @@ const TOOL_META: Record<DiagTool, {
|
||||
|
||||
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 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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useDataSource } from "@/lib/data-source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { servers as mockServers, type Server } from "@/lib/data"
|
||||
import { PlusIcon, SaveIcon, TrashIcon, SearchIcon, XIcon, PencilIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon, CheckIcon } from "lucide-react"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
|
||||
interface BackendServer {
|
||||
id: number
|
||||
@@ -91,23 +92,7 @@ interface RouteGroup {
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { servers } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import {
|
||||
@@ -40,23 +41,7 @@ import {
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-96
@@ -5,71 +5,21 @@ import { PageHeader } from "@/components/page-header"
|
||||
import { StatusBadge } from "@/components/status-badge"
|
||||
import { servers as initialServers } 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 ──────────────────────────────────────────────────────
|
||||
|
||||
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 { cn } from "@/lib/utils"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
@@ -373,7 +323,6 @@ type SheetMode = "add" | "edit"
|
||||
export default function ServersPage() {
|
||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||
const isLive = mode === "live" && backendStatus === true
|
||||
const apiFetch = makeApiFetch(backendUrl)
|
||||
|
||||
const [serverList, setServerList] = useState<Server[]>(initialServers)
|
||||
const [_backendOk, setBackendOk] = useState(false)
|
||||
@@ -402,10 +351,10 @@ export default function ServersPage() {
|
||||
})
|
||||
return
|
||||
}
|
||||
apiFetch<BackendServer[]>("/api/servers")
|
||||
listServers(backendUrl)
|
||||
.then(data => {
|
||||
setBackendOk(true)
|
||||
setServerList(data.map(toFrontend))
|
||||
setServerList(data.map(toFrontendServer))
|
||||
})
|
||||
.catch(() => {
|
||||
setBackendOk(false)
|
||||
@@ -437,7 +386,7 @@ export default function ServersPage() {
|
||||
// Fetch full server details (including credentials) from backend
|
||||
if (isLive) {
|
||||
try {
|
||||
const full = await apiFetch<BackendServer>(`/api/servers/${s.id}`)
|
||||
const full = await getServer(backendUrl, s.id)
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
username: full.username ?? "",
|
||||
@@ -458,7 +407,7 @@ export default function ServersPage() {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const payload = {
|
||||
const payload: ServerCreate = {
|
||||
host: form.host,
|
||||
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
||||
username: form.username,
|
||||
@@ -479,17 +428,12 @@ export default function ServersPage() {
|
||||
if (isLive) {
|
||||
try {
|
||||
if (sheetMode === "edit" && editingId) {
|
||||
const updated = await apiFetch<BackendServer>(
|
||||
`/api/servers/${editingId}`,
|
||||
{ method: "PUT", body: JSON.stringify(payload) },
|
||||
)
|
||||
setServerList(list => list.map(s => s.id === editingId ? toFrontend(updated) : s))
|
||||
const updatedPayload: ServerUpdate = payload
|
||||
const updated = await updateServer(backendUrl, editingId, updatedPayload)
|
||||
setServerList(list => list.map(s => s.id === editingId ? toFrontendServer(updated) : s))
|
||||
} else {
|
||||
const created = await apiFetch<BackendServer>(
|
||||
"/api/servers",
|
||||
{ method: "POST", body: JSON.stringify(payload) },
|
||||
)
|
||||
setServerList(list => [...list, toFrontend(created)])
|
||||
const created = await createServer(backendUrl, payload)
|
||||
setServerList(list => [...list, toFrontendServer(created)])
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Ошибка сохранения:", e)
|
||||
@@ -540,7 +484,7 @@ export default function ServersPage() {
|
||||
async function handleDelete(id: string) {
|
||||
if (isLive) {
|
||||
try {
|
||||
await apiFetch(`/api/servers/${id}`, { method: "DELETE" })
|
||||
await deleteServer(backendUrl, id)
|
||||
} catch (e) {
|
||||
console.error("Ошибка удаления:", e)
|
||||
}
|
||||
@@ -552,10 +496,10 @@ export default function ServersPage() {
|
||||
if (!isLive) return
|
||||
setPollingIds(p => new Set(p).add(id))
|
||||
try {
|
||||
await apiFetch(`/api/servers/${id}/poll`, { method: "POST" })
|
||||
await pollServer(backendUrl, id)
|
||||
// Reload full list to get updated status/version
|
||||
const data = await apiFetch<BackendServer[]>("/api/servers")
|
||||
setServerList(data.map(toFrontend))
|
||||
const data = await listServers(backendUrl)
|
||||
setServerList(data.map(toFrontendServer))
|
||||
} catch (e) {
|
||||
console.error("Poll error:", e)
|
||||
} finally {
|
||||
@@ -568,9 +512,9 @@ export default function ServersPage() {
|
||||
setPollAllBusy(true)
|
||||
try {
|
||||
const ids = serverList.map(s => s.id)
|
||||
await Promise.all(ids.map(id => apiFetch(`/api/servers/${id}/poll`, { method: "POST" }).catch(() => {})))
|
||||
const data = await apiFetch<BackendServer[]>("/api/servers")
|
||||
setServerList(data.map(toFrontend))
|
||||
await Promise.all(ids.map(id => pollServer(backendUrl, id).catch(() => {})))
|
||||
const data = await listServers(backendUrl)
|
||||
setServerList(data.map(toFrontendServer))
|
||||
} finally {
|
||||
setPollAllBusy(false)
|
||||
}
|
||||
@@ -582,20 +526,15 @@ export default function ServersPage() {
|
||||
}
|
||||
setTestState("testing"); setTestMsg("")
|
||||
try {
|
||||
const res = await fetch(backendUrl.replace(/\/$/, "") + "/api/servers/test-connection", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
host: form.host,
|
||||
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
||||
useSsl: form.proto === "https",
|
||||
verifySsl: form.verifySsl,
|
||||
apiPath: form.apiPath || "/rest",
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
}),
|
||||
const data = await testServerConnection(backendUrl, {
|
||||
host: form.host,
|
||||
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
|
||||
useSsl: form.proto === "https",
|
||||
verifySsl: form.verifySsl,
|
||||
apiPath: form.apiPath || "/rest",
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
})
|
||||
const data = await res.json() as { success: boolean; message: string }
|
||||
if (data.success) {
|
||||
setTestState("ok"); setTestMsg(data.message)
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ServerIcon, LayoutDashboardIcon, RefreshCwIcon, CableIcon, LoaderCircleIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -152,23 +153,7 @@ type NavSection = typeof SECTIONS_NAV[number]
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -126,23 +127,7 @@ interface LiveTrafficInterface {
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 type { PingProbe } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import {
|
||||
RefreshCwIcon, PlusIcon, SearchIcon, XIcon,
|
||||
ChevronDownIcon, ChevronRightIcon,
|
||||
@@ -63,23 +64,7 @@ function mockProbesWithSavedStars(base: PingProbe[]): PingProbe[] {
|
||||
|
||||
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) {
|
||||
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>
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { z } from "zod"
|
||||
import {
|
||||
getAlertsMeta,
|
||||
getTelegramBotToken,
|
||||
@@ -13,90 +12,11 @@ import {
|
||||
sendTelegramAlertMessage,
|
||||
updateTelegramSettings,
|
||||
} from "../services/alerts-service.js"
|
||||
|
||||
const AlertTypeSchema = z.enum([
|
||||
"gre-tunnel",
|
||||
"bgp-peer",
|
||||
"bgp-prefix",
|
||||
"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(),
|
||||
})
|
||||
import {
|
||||
putRulesSchema,
|
||||
putTelegramSchema,
|
||||
testTelegramSchema,
|
||||
} from "../../../packages/contracts/dist/alerts.js"
|
||||
|
||||
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/alerts", async (_req, reply) => {
|
||||
@@ -112,7 +32,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
})
|
||||
|
||||
app.put("/alerts/rules", async (req, reply) => {
|
||||
const parsed = PutRulesSchema.safeParse(req.body)
|
||||
const parsed = putRulesSchema.safeParse(req.body)
|
||||
if (!parsed.success) {
|
||||
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) => {
|
||||
const parsed = PutTelegramSchema.safeParse(req.body)
|
||||
const parsed = putTelegramSchema.safeParse(req.body)
|
||||
if (!parsed.success) {
|
||||
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) => {
|
||||
const parsed = TestTelegramSchema.safeParse(req.body ?? {})
|
||||
const parsed = testTelegramSchema.safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
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 { parseBgpSessions } from "../services/bgp-parse-sessions.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"
|
||||
|
||||
const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// 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) => {
|
||||
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" })
|
||||
try {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
@@ -44,9 +45,10 @@ const bgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// GET /api/servers/:id/bgp/sessions — single server
|
||||
app.get("/servers/:id/bgp/sessions", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.where(eq(servers.id, params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
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 { servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { ServerIdParamSchema } from "../types/server.js"
|
||||
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||
import type {
|
||||
RosIpAddress, RosInterface, RosResource, RosIdentity, RosIpRoute,
|
||||
RosFirewallFilter, RosLogEntry, RosBgpSession, RosPingResult,
|
||||
@@ -313,9 +313,10 @@ const execRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
"/servers/:id/exec",
|
||||
{ schema: { params: ServerIdParamSchema, body: ExecBodySchema } },
|
||||
async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.where(eq(servers.id, params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
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 { serverSnapshots, servers, trafficSamples, uptimeSpeedProbes } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { ServerIdParamSchema } from "../types/server.js"
|
||||
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||
import type {
|
||||
RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
|
||||
RosBfdSession,
|
||||
@@ -630,9 +630,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// GET /api/servers/:id/ospf — single server OSPF + BFD data
|
||||
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.where(eq(servers.id, params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
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",
|
||||
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
||||
async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.where(eq(servers.id, params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
@@ -688,9 +690,10 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
"/servers/:id/ospf/optimize",
|
||||
{ schema: { params: ServerIdParamSchema, body: OspfOptimizeBodySchema } },
|
||||
async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.where(eq(servers.id, params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
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 { db } from "../db/index.js"
|
||||
import { servers, serverSnapshots } from "../db/schema.js"
|
||||
import type { ServerRead, WanUplinkRead } from "../types/server.js"
|
||||
import {
|
||||
ServerCreateSchema,
|
||||
ServerUpdateSchema,
|
||||
ServerIdParamSchema,
|
||||
SnapshotsQuerySchema,
|
||||
TestConnectionSchema,
|
||||
type ServerCreateRequest,
|
||||
type ServerIdParams,
|
||||
type ServerUpdateRequest,
|
||||
type SnapshotsQuery,
|
||||
type TestConnectionRequest,
|
||||
} 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 { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
type SnapshotRow = typeof serverSnapshots.$inferSelect
|
||||
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
||||
/** 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]
|
||||
}
|
||||
import {
|
||||
createServer,
|
||||
deleteServer,
|
||||
getServerReadById,
|
||||
listServerSnapshots,
|
||||
listServersRead,
|
||||
updateServer,
|
||||
} from "../modules/servers/service/servers-service.js"
|
||||
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
|
||||
|
||||
// ── plugin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -97,7 +30,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// POST /api/servers/test-connection — check credentials before saving
|
||||
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 })
|
||||
|
||||
try {
|
||||
@@ -144,17 +77,13 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// GET /api/servers
|
||||
app.get("/", async (_req, reply) => {
|
||||
const all = db.select().from(servers).all()
|
||||
const result: ServerRead[] = all.map(s => toServerRead(s, getLatestSnapshot(s.id)))
|
||||
return reply.send(result)
|
||||
return reply.send(listServersRead())
|
||||
})
|
||||
|
||||
// GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
|
||||
app.get("/:id/ros-src-address", { 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 = getServerRowById(params.id)
|
||||
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
const ipv4 = await resolveRosSrcIpv4(server.host)
|
||||
@@ -163,30 +92,15 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
// POST /api/servers
|
||||
app.post("/", { schema: { body: ServerCreateSchema } }, async (req, reply) => {
|
||||
const now = new Date().toISOString()
|
||||
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))
|
||||
return reply.status(201).send(createServer(req.body as ServerCreateRequest))
|
||||
})
|
||||
|
||||
// GET /api/servers/:id
|
||||
app.get("/: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 = getServerReadById(params.id)
|
||||
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
|
||||
@@ -194,58 +108,30 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
"/:id",
|
||||
{ schema: { params: ServerIdParamSchema, body: ServerUpdateSchema } },
|
||||
async (req, reply) => {
|
||||
const existing = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
const params = req.params as ServerIdParams
|
||||
const existing = getServerReadById(params.id)
|
||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
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)))
|
||||
return reply.send(updateServer(params.id, req.body as ServerUpdateRequest))
|
||||
},
|
||||
)
|
||||
|
||||
// DELETE /api/servers/:id
|
||||
app.delete("/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const existing = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
const params = req.params as ServerIdParams
|
||||
const existing = getServerReadById(params.id)
|
||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
db.delete(servers).where(eq(servers.id, req.params.id)).run()
|
||||
deleteServer(params.id)
|
||||
return reply.status(204).send()
|
||||
})
|
||||
|
||||
// POST /api/servers/:id/poll
|
||||
app.post("/:id/poll", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const existing = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
const params = req.params as ServerIdParams
|
||||
const existing = getServerReadById(params.id)
|
||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
try {
|
||||
const snap = await pollServer(req.params.id)
|
||||
const snap = await pollServer(params.id)
|
||||
return reply.send(snap)
|
||||
} catch (err) {
|
||||
return reply.status(500).send({ error: (err as Error).message })
|
||||
@@ -257,22 +143,11 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
"/:id/snapshots",
|
||||
{ schema: { params: ServerIdParamSchema, querystring: SnapshotsQuerySchema } },
|
||||
async (req, reply) => {
|
||||
const existing = db
|
||||
.select().from(servers)
|
||||
.where(eq(servers.id, req.params.id))
|
||||
.limit(1).all()[0]
|
||||
|
||||
const params = req.params as ServerIdParams
|
||||
const query = req.query as SnapshotsQuery
|
||||
const existing = getServerReadById(params.id)
|
||||
if (!existing) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
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))
|
||||
return reply.send(listServerSnapshots(params.id, query.limit))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+29
-105
@@ -1,4 +1,16 @@
|
||||
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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -49,116 +61,28 @@ export interface RosIpAddress {
|
||||
|
||||
// ── Zod schemas for API validation ────────────────────────────────────────────
|
||||
|
||||
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 = serverCreateSchema
|
||||
export const ServerUpdateSchema = serverUpdateSchema
|
||||
export const TestConnectionSchema = testConnectionSchema
|
||||
export const ServerIdParamSchema = serverIdParamSchema
|
||||
export const SnapshotsQuerySchema = snapshotsQuerySchema
|
||||
|
||||
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: 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),
|
||||
})
|
||||
// Request payload types for route handlers.
|
||||
// (Type-provider-zod in this repo sometimes falls back to `unknown` during cross-package schema wiring.)
|
||||
export type ServerIdParams = z.infer<typeof ServerIdParamSchema>
|
||||
export type TestConnectionRequest = z.infer<typeof TestConnectionSchema>
|
||||
export type SnapshotsQuery = z.infer<typeof SnapshotsQuerySchema>
|
||||
export type ServerCreateRequest = z.infer<typeof ServerCreateSchema>
|
||||
export type ServerUpdateRequest = z.infer<typeof ServerUpdateSchema>
|
||||
|
||||
// ── Response types (what the API returns) ─────────────────────────────────────
|
||||
|
||||
/** Flat server response — mirrors the frontend's Server interface from lib/data.ts */
|
||||
export interface WanUplinkRead {
|
||||
id: string
|
||||
name: string
|
||||
isp: string
|
||||
iface: string
|
||||
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
|
||||
}
|
||||
export type WanUplinkRead = ContractWanUplinkRead
|
||||
export type ServerCreate = ContractServerCreate
|
||||
export type ServerUpdate = ContractServerUpdate
|
||||
export type ServerRead = ContractServerRead
|
||||
export type SnapshotRead = ContractSnapshotRead
|
||||
|
||||
// ── BGP types ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"rootDir": "..",
|
||||
"strict": true,
|
||||
"esModuleInterop": 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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export type AlertEngineRuleDiagSnapshot = {
|
||||
ruleId: string
|
||||
inGroup: boolean
|
||||
evalHit: boolean
|
||||
hitTransition?: "problem" | "recovery" | "neutral"
|
||||
hitMessage?: string
|
||||
stabilityOk: boolean
|
||||
cooldownOk: 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