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:
Denozordec
2026-05-07 12:29:04 +07:00
co-authored by Cursor
parent bdb9b72fac
commit 5f31bb47fb
81 changed files with 11976 additions and 1239 deletions
+118 -9
View File
@@ -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}