fix(api): проверить каталог speed-проб и изолировать источник
This commit is contained in:
1 parent
f30319ac55
commit
d4f2961f76
13 files changed
+123
-51
No files matched your search
+17
-14
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { speedHistoryResponseSchema } from "@mmapp/contracts"
|
||||
import { speedHistoryResponseSchema, speedProbesResponseSchema } from "@mmapp/contracts"
|
||||
|
||||
import probeStyles from "./probe-rows.module.css"
|
||||
|
||||
@@ -1110,6 +1110,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
const [speedProbes, setSpeedProbes] = useState<SpeedProbeRow[]>([])
|
||||
const speedReadRevision = useRef(0)
|
||||
const [speedReads, setSpeedReads] = useState<Record<"probes" | "history", { receivedAt: number | null; error: string | null }>>({ probes: { receivedAt: null, error: null }, history: { receivedAt: null, error: null } })
|
||||
const speedCatalogReady = !liveApi || (speedReads.probes.receivedAt !== null && speedReads.probes.error === null)
|
||||
const [speedIfaces, setSpeedIfaces] = useState<Record<string, Array<{ name: string; running: boolean; disabled: boolean; addresses: string[] }>>>({})
|
||||
const [speedCollapsed, setSpeedCollapsed] = useState<Set<string>>(new Set())
|
||||
const [speedSheetOpen, setSpeedSheetOpen] = useState(false)
|
||||
@@ -1178,9 +1179,9 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
}
|
||||
await Promise.all([
|
||||
read("probes", async () => {
|
||||
const sp = await apiFetch<{ probes: SpeedProbeRow[] }>("/api/uptime/speed-probes")
|
||||
if (!Array.isArray(sp.probes)) throw new Error("Некорректный список speed-проб")
|
||||
if (current()) setSpeedProbes(sp.probes)
|
||||
const result = speedProbesResponseSchema.safeParse(await apiFetch<unknown>("/api/uptime/speed-probes"))
|
||||
if (!result.success) throw new Error("Данные проб скорости не подтверждены сервером.")
|
||||
if (current()) setSpeedProbes(result.data.probes)
|
||||
}),
|
||||
read("history", async () => {
|
||||
const result = speedHistoryResponseSchema.safeParse(await apiFetch<unknown>("/api/uptime/speed-test/runs"))
|
||||
@@ -1522,6 +1523,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
|
||||
const persistSpeedProbes = useCallback(async (rows: SpeedProbeRow[]) => {
|
||||
if (!liveApi) return
|
||||
if (!speedCatalogReady) throw new Error("Каталог speed-проб не подтверждён. Обновите данные перед сохранением.")
|
||||
await apiFetch("/api/uptime/speed-probes", {
|
||||
method: "PUT",
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
@@ -1544,7 +1546,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
})),
|
||||
}),
|
||||
})
|
||||
}, [apiFetch, liveApi])
|
||||
}, [apiFetch, liveApi, speedCatalogReady])
|
||||
|
||||
async function saveProbeForm(save: () => Promise<void>, confirm: () => void) {
|
||||
if (!requireWrite()) return
|
||||
@@ -1885,7 +1887,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
}
|
||||
|
||||
const openAddSpeedSheet = () => {
|
||||
if (!requireWrite()) return
|
||||
if (!requireWrite() || !speedCatalogReady) return
|
||||
setProbeSaveError(null)
|
||||
setEditingSpeedProbeId(null)
|
||||
const src = selectableSources[0]?.id ?? ""
|
||||
@@ -2001,7 +2003,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
</Button>
|
||||
)}
|
||||
{tab === "speed" && (
|
||||
<Button size="sm" disabled={probeSaving || !canWrite} onClick={openAddSpeedSheet}>
|
||||
<Button size="sm" disabled={probeSaving || !canWrite || !speedCatalogReady} onClick={openAddSpeedSheet}>
|
||||
<PlusIcon className="size-4" />Новая speed-проба
|
||||
</Button>
|
||||
)}
|
||||
@@ -2077,7 +2079,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
<span className="flex items-center gap-2">
|
||||
<ArrowUpDownIcon className="size-3.5" />
|
||||
<span>Скорость</span>
|
||||
<span className="text-[10px] font-mono opacity-50">{speedProbes.length}</span>
|
||||
<span className="text-[10px] font-mono opacity-50">{liveApi && speedReads.probes.receivedAt === null ? "—" : speedProbes.length}</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -2096,7 +2098,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
const state = speedReads[resource], label = resource === "probes" ? "Speed-пробы" : "История тестов"
|
||||
return <div key={resource}>
|
||||
<p role="status" className="text-xs text-muted-foreground">{label}: {state.receivedAt ? "получено " + new Date(state.receivedAt).toLocaleString("ru-RU") : "данные пока неизвестны"}</p>
|
||||
{state.error && <ContextAlert variant="warning" aria-label={"Чтение скорости: " + resource}><ContextTitle>{label}</ContextTitle><ContextDescription>{state.error} {state.receivedAt ? "Последние данные сохранены." : "Данные пока неизвестны."}</ContextDescription></ContextAlert>}
|
||||
{state.error && <ContextAlert variant="warning" aria-label={"Чтение скорости: " + resource}><ContextTitle>{label}</ContextTitle><ContextDescription>{state.error} {state.receivedAt ? "Последние данные сохранены." : "Данные пока неизвестны."}{resource === "probes" ? " Изменение каталога недоступно до успешного обновления." : ""}</ContextDescription></ContextAlert>}
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
@@ -2171,7 +2173,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
<p className="text-xs text-muted-foreground max-w-xs">
|
||||
Создайте пробу через кнопку «Новая speed-проба» в шапке страницы
|
||||
</p>
|
||||
<Button size="sm" variant="outline" className="mt-1" disabled={probeSaving || !canWrite} onClick={openAddSpeedSheet}>
|
||||
<Button size="sm" variant="outline" className="mt-1" disabled={probeSaving || !canWrite || !speedCatalogReady} onClick={openAddSpeedSheet}>
|
||||
<PlusIcon className="size-4" />Новая speed-проба
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2229,7 +2231,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
className={cn("px-4 py-3 hover:bg-muted/20 transition-colors", !probe.enabled && "opacity-50")}>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* enable toggle */}
|
||||
<FormToggle disabled={probeSaving || !canWrite} checked={probe.enabled} onChange={(v) => updateSpeedProbe(probe.id, { enabled: v })} />
|
||||
<FormToggle ariaLabel={"Включить speed-пробу " + server.name + " → " + (dst?.name ?? probe.dstServerId)} disabled={probeSaving || !canWrite || !speedCatalogReady} checked={probe.enabled} onChange={(v) => updateSpeedProbe(probe.id, { enabled: v })} />
|
||||
|
||||
{/* route: src → dst */}
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
@@ -2266,12 +2268,12 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
Запустить
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-7 text-muted-foreground hover:text-foreground"
|
||||
disabled={probeSaving || !canWrite} aria-label="Редактировать speed-пробу" onClick={() => openEditSpeedSheet(probe)}
|
||||
disabled={probeSaving || !canWrite || !speedCatalogReady} aria-label="Редактировать speed-пробу" onClick={() => openEditSpeedSheet(probe)}
|
||||
title="Редактировать speed-пробу">
|
||||
<PencilIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-7 text-muted-foreground hover:text-destructive"
|
||||
disabled={probeSaving || !canWrite} aria-label="Удалить speed-пробу" onClick={() => deleteSpeedProbe(probe.id)}>
|
||||
disabled={probeSaving || !canWrite || !speedCatalogReady} aria-label="Удалить speed-пробу" onClick={() => deleteSpeedProbe(probe.id)}>
|
||||
<TrashIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2996,11 +2998,12 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
|
||||
}
|
||||
|
||||
function UptimeRoute() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const params = useSearchParams()
|
||||
const id = params.get("probe")
|
||||
const rest = new URLSearchParams(params.toString())
|
||||
rest.delete("probe")
|
||||
return <UptimePageContent key={id === null ? "list" : "probe:" + id} selectedProbeId={id} allProbesHref={"/uptime" + (rest.size ? "?" + rest : "")} />
|
||||
return <UptimePageContent key={JSON.stringify([id, mode, backendUrl])} selectedProbeId={id} allProbesHref={"/uptime" + (rest.size ? "?" + rest : "")} />
|
||||
}
|
||||
|
||||
export default function UptimePage() {
|
||||
|
||||
Reference in new issue
Block a user