chore: synchronize pending app/backend updates and repository hygiene
Includes current frontend and backend work in progress and removes generated artifacts from tracking to keep the repository clean for дальнейшая разработка. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+118
-9
@@ -12,6 +12,7 @@ import {
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { readStoredRouteOptimizerSettings } from "@/lib/route-optimizer-data"
|
||||
|
||||
// ─── types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -119,6 +120,28 @@ interface BackendOspfAll {
|
||||
bfdSessions: BackendBfdSession[]
|
||||
}
|
||||
|
||||
function isRefInterfaceName(name: string): boolean {
|
||||
return /^\(ref\s+\*.+\)$/.test(name.trim())
|
||||
}
|
||||
|
||||
interface BackendOspfOptimizeResponse {
|
||||
serverId: number
|
||||
serverName: string
|
||||
pingWeight: number
|
||||
optimizedCount: number
|
||||
applied: Array<{ interface: string; from: number; to: number }>
|
||||
interfaces: Array<{
|
||||
id: string
|
||||
interface: string
|
||||
currentCost: number
|
||||
optimalCost: number
|
||||
score: number
|
||||
pingMs: number
|
||||
dlMbps: number
|
||||
ulMbps: number
|
||||
}>
|
||||
}
|
||||
|
||||
// ─── backend → frontend mappers ───────────────────────────────────────────────
|
||||
|
||||
function backendToNeighbor(b: BackendNeighbor, ifaceMap: Map<string, number>): OspfNeighbor {
|
||||
@@ -625,11 +648,25 @@ function NodeDetailPanel({
|
||||
|
||||
// ─── interfaces tab ───────────────────────────────────────────────────────────
|
||||
|
||||
function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isLive: boolean }) {
|
||||
function InterfacesTab({
|
||||
items: initialItems,
|
||||
isLive,
|
||||
filterServerId,
|
||||
backendUrl,
|
||||
onLiveDataRefresh,
|
||||
}: {
|
||||
items: OspfItem[]
|
||||
isLive: boolean
|
||||
filterServerId: string
|
||||
backendUrl: string
|
||||
onLiveDataRefresh: () => void
|
||||
}) {
|
||||
const [items, setItems] = useState<OspfItem[]>(initialItems)
|
||||
const [dragging, setDragging] = useState<string | null>(null)
|
||||
const [dragOver, setDragOver] = useState<string | null>(null)
|
||||
const [toast, setToast] = useState<string | null>(null)
|
||||
const [optimizing, setOptimizing] = useState(false)
|
||||
const [liveOptimalCost, setLiveOptimalCost] = useState<Record<string, number>>({})
|
||||
|
||||
// Sync with live data when it changes
|
||||
useEffect(() => {
|
||||
@@ -660,17 +697,67 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
|
||||
if (isLive) return {}
|
||||
const out: Record<string, { optimalCost: number; prob: number }> = {}
|
||||
grouped.forEach(router => {
|
||||
router.areas.forEach(ag => {
|
||||
const ranked = [...ag.items]
|
||||
.map(item => ({ item, prob: OPTIMIZER_PROB[`${router.routerKey}::${item.interfaceName.toUpperCase()}`] ?? 0 }))
|
||||
.sort((a, b) => b.prob - a.prob || a.item.interfaceName.localeCompare(b.item.interfaceName))
|
||||
ranked.forEach(({ item, prob }, idx) => { out[item.key] = { optimalCost: (idx + 1) * COST_STEP, prob } })
|
||||
const ranked = router.areas
|
||||
.flatMap(ag => ag.items)
|
||||
.map(item => ({ item, prob: OPTIMIZER_PROB[`${router.routerKey}::${item.interfaceName.toUpperCase()}`] ?? 0 }))
|
||||
.sort((a, b) => b.prob - a.prob || a.item.interfaceName.localeCompare(b.item.interfaceName))
|
||||
|
||||
// В рамках одного роутера выдаём строго уникальные optimal cost.
|
||||
ranked.forEach(({ item, prob }, idx) => {
|
||||
out[item.key] = { optimalCost: (idx + 1) * COST_STEP, prob }
|
||||
})
|
||||
})
|
||||
return out
|
||||
}, [grouped, isLive])
|
||||
|
||||
const needsOptimize = !isLive && items.some(item => hints[item.key] && hints[item.key].optimalCost !== item.cost)
|
||||
const canOptimizeLive = isLive && filterServerId !== "all"
|
||||
const uniqueLiveFallbackOpt = useMemo(() => {
|
||||
const out: Record<string, number> = {}
|
||||
const byRouter: Record<string, OspfItem[]> = {}
|
||||
items.forEach((item) => {
|
||||
if (!byRouter[item.routerKey]) byRouter[item.routerKey] = []
|
||||
byRouter[item.routerKey].push(item)
|
||||
})
|
||||
Object.values(byRouter).forEach((routerItems) => {
|
||||
routerItems
|
||||
.slice()
|
||||
.sort((a, b) => a.cost - b.cost || a.interfaceName.localeCompare(b.interfaceName))
|
||||
.forEach((item, idx) => {
|
||||
out[item.key] = (idx + 1) * COST_STEP
|
||||
})
|
||||
})
|
||||
return out
|
||||
}, [items])
|
||||
|
||||
async function handleLiveOptimize() {
|
||||
if (!canOptimizeLive) {
|
||||
showToast("Выберите конкретный сервер для оптимизации OSPF")
|
||||
return
|
||||
}
|
||||
const ra = readStoredRouteOptimizerSettings()
|
||||
setOptimizing(true)
|
||||
try {
|
||||
const r = await fetch(`${backendUrl}/api/servers/${filterServerId}/ospf/optimize`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pingWeight: ra.pingWeight }),
|
||||
})
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
const data = await r.json() as BackendOspfOptimizeResponse
|
||||
const byKey: Record<string, number> = {}
|
||||
data.interfaces.forEach((row) => {
|
||||
byKey[`${data.serverId}-${row.id}`] = row.optimalCost
|
||||
})
|
||||
setLiveOptimalCost(byKey)
|
||||
showToast(`OSPF оптимизация применена: ${data.optimizedCount} интерфейсов`)
|
||||
onLiveDataRefresh()
|
||||
} catch (err) {
|
||||
showToast(`Ошибка оптимизации OSPF: ${err instanceof Error ? err.message : String(err)}`)
|
||||
} finally {
|
||||
setOptimizing(false)
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop(routerKey: string, area: string, targetKey: string) {
|
||||
if (!dragging || dragging === targetKey) { setDragging(null); setDragOver(null); return }
|
||||
@@ -704,6 +791,12 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
|
||||
<WandSparklesIcon className="size-4" />Оптимизировать
|
||||
</Button>
|
||||
)}
|
||||
{canOptimizeLive && (
|
||||
<Button variant="outline" size="sm" onClick={handleLiveOptimize} disabled={optimizing}>
|
||||
<WandSparklesIcon className={cn("size-4", optimizing && "animate-spin")} />
|
||||
{optimizing ? "Оптимизация OSPF…" : "Оптимизировать OSPF"}
|
||||
</Button>
|
||||
)}
|
||||
{!isLive && (
|
||||
<Button size="sm" onClick={() => showToast("OSPF Interface Templates применены")}>
|
||||
<SaveIcon className="size-4" />Сохранить
|
||||
@@ -750,6 +843,8 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
|
||||
{ag.items.map(item => {
|
||||
const hint = hints[item.key]
|
||||
const matches = hint && hint.optimalCost === item.cost
|
||||
const liveOptimal = liveOptimalCost[item.key] ?? uniqueLiveFallbackOpt[item.key] ?? item.cost
|
||||
const costDiffers = liveOptimal !== item.cost
|
||||
return (
|
||||
<div key={item.key}
|
||||
draggable={!isLive}
|
||||
@@ -779,7 +874,11 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
|
||||
: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
|
||||
}>opt {hint.optimalCost}</Chip>
|
||||
)}
|
||||
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cost {item.cost}</Chip>
|
||||
<Chip color="bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20">cur {item.cost}</Chip>
|
||||
<Chip color={costDiffers
|
||||
? "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
|
||||
: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
|
||||
}>opt {liveOptimal}</Chip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -1184,7 +1283,9 @@ export default function OspfPage() {
|
||||
ifaceMap.set(`${iface.serverId}::${iface.interface}`, iface.cost)
|
||||
}
|
||||
|
||||
const items = liveData.interfaces.map(backendToItem)
|
||||
const items = liveData.interfaces
|
||||
.map(backendToItem)
|
||||
.filter((item) => !isRefInterfaceName(item.interfaceName))
|
||||
const neighbors = liveData.neighbors.map(b => backendToNeighbor(b, ifaceMap))
|
||||
const bfdSessions = (liveData.bfdSessions ?? []).map(backendToBfdSession)
|
||||
|
||||
@@ -1389,7 +1490,15 @@ export default function OspfPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === "interfaces" && <InterfacesTab items={displayItems} isLive={isLive} />}
|
||||
{activeTab === "interfaces" && (
|
||||
<InterfacesTab
|
||||
items={displayItems}
|
||||
isLive={isLive}
|
||||
filterServerId={filterServerId}
|
||||
backendUrl={backendUrl}
|
||||
onLiveDataRefresh={() => setFetchTick(t => t + 1)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "neighbors" && (
|
||||
<NeighborsTab
|
||||
neighbors={displayNeighbors}
|
||||
|
||||
Reference in New Issue
Block a user