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
-29
@@ -30,6 +30,7 @@ import {
|
||||
GitMergeIcon,
|
||||
TerminalIcon,
|
||||
SettingsIcon,
|
||||
DatabaseIcon,
|
||||
CableIcon,
|
||||
ScanLineIcon,
|
||||
BellIcon,
|
||||
@@ -39,6 +40,13 @@ import {
|
||||
BoxIcon,
|
||||
BadgeCheckIcon,
|
||||
} from "lucide-react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useEvoBGP } from "@/lib/evobgp-context"
|
||||
import {
|
||||
formatSidebarBadgeCount,
|
||||
mockSidebarBadgesByUrl,
|
||||
type SidebarCountsDto,
|
||||
} from "@/lib/sidebar-badges"
|
||||
|
||||
// ─── Command palette trigger button ──────────────────────────────────────────
|
||||
|
||||
@@ -58,69 +66,154 @@ function CommandPaletteButton() {
|
||||
)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
type NavItemBase = { title: string; url: string; icon: React.ReactNode }
|
||||
|
||||
const navGroups: NavGroup[] = [
|
||||
const navStructure: { label: string; items: NavItemBase[] }[] = [
|
||||
{
|
||||
label: "Обзор",
|
||||
items: [
|
||||
{ title: "Дашборд", url: "/dashboard", icon: <LayoutDashboardIcon /> },
|
||||
{ title: "Трафик", url: "/traffic", icon: <ActivityIcon /> },
|
||||
{ title: "Карта сети", url: "/network-map", icon: <MapIcon /> },
|
||||
{ title: "Мониторинг", url: "/uptime", icon: <HeartPulseIcon />, badge: "8" },
|
||||
{ title: "Дашборд", url: "/dashboard", icon: <LayoutDashboardIcon /> },
|
||||
{ title: "Трафик", url: "/traffic", icon: <ActivityIcon /> },
|
||||
{ title: "Карта сети", url: "/network-map", icon: <MapIcon /> },
|
||||
{ title: "Мониторинг", url: "/uptime", icon: <HeartPulseIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Данные",
|
||||
items: [
|
||||
{ title: "Домены", url: "/domains", icon: <GlobeIcon />, badge: "1.8к" },
|
||||
{ title: "IP-диапазоны", url: "/ip-ranges", icon: <NetworkIcon />, badge: "4.2к" },
|
||||
{ title: "ASN", url: "/asns", icon: <LayersIcon /> },
|
||||
{ title: "Communities", url: "/communities", icon: <TagIcon /> },
|
||||
{ title: "Домены", url: "/domains", icon: <GlobeIcon /> },
|
||||
{ title: "IP-диапазоны", url: "/ip-ranges", icon: <NetworkIcon /> },
|
||||
{ title: "ASN", url: "/asns", icon: <LayersIcon /> },
|
||||
{ title: "Communities", url: "/communities", icon: <TagIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Управление",
|
||||
items: [
|
||||
{ title: "Серверы", url: "/servers", icon: <ServerIcon />, badge: "9" },
|
||||
{ title: "Фильтры", url: "/filters", icon: <FilterIcon />, badge: "6" },
|
||||
{ title: "Серверы", url: "/servers", icon: <ServerIcon /> },
|
||||
{ title: "Фильтры", url: "/filters", icon: <FilterIcon /> },
|
||||
{ title: "Рекурсивные маршруты", url: "/recursive-routes", icon: <RouteIcon /> },
|
||||
{ title: "Firewall", url: "/firewall", icon: <ShieldIcon /> },
|
||||
{ title: "WireGuard", url: "/wireguard", icon: <ShieldCheckIcon />, badge: "4" },
|
||||
{ title: "GRE-туннели", url: "/gre", icon: <CableIcon />, badge: "6" },
|
||||
{ title: "VXLAN", url: "/vxlan", icon: <NetworkIcon /> },
|
||||
{ title: "Контейнеры", url: "/containers", icon: <BoxIcon />, badge: "5" },
|
||||
{ title: "Сертификаты", url: "/certificates", icon: <BadgeCheckIcon />, badge: "2" },
|
||||
{ title: "Бэкапы", url: "/backups", icon: <HardDriveIcon /> },
|
||||
{ title: "Firewall", url: "/firewall", icon: <ShieldIcon /> },
|
||||
{ title: "WireGuard", url: "/wireguard", icon: <ShieldCheckIcon /> },
|
||||
{ title: "GRE-туннели", url: "/gre", icon: <CableIcon /> },
|
||||
{ title: "VXLAN", url: "/vxlan", icon: <NetworkIcon /> },
|
||||
{ title: "Контейнеры", url: "/containers", icon: <BoxIcon /> },
|
||||
{ title: "Сертификаты", url: "/certificates", icon: <BadgeCheckIcon /> },
|
||||
{ title: "Бэкапы", url: "/backups", icon: <HardDriveIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Инструменты",
|
||||
items: [
|
||||
{ title: "Оптимизатор маршрутов", url: "/route-optimizer", icon: <RouteIcon /> },
|
||||
{ title: "OSPF", url: "/ospf", icon: <GitForkIcon /> },
|
||||
{ title: "BGP", url: "/bgp", icon: <GitMergeIcon />, badge: "12" },
|
||||
{ title: "Диагностика", url: "/probes", icon: <ScanLineIcon /> },
|
||||
{ title: "Терминал", url: "/terminal", icon: <TerminalIcon /> },
|
||||
{ title: "OSPF", url: "/ospf", icon: <GitForkIcon /> },
|
||||
{ title: "BGP", url: "/bgp", icon: <GitMergeIcon /> },
|
||||
{ title: "Диагностика", url: "/probes", icon: <ScanLineIcon /> },
|
||||
{ title: "Терминал", url: "/terminal", icon: <TerminalIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Система",
|
||||
items: [
|
||||
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
|
||||
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
|
||||
{ title: "Оповещения", url: "/alerts", icon: <BellIcon /> },
|
||||
{ title: "Сбор данных", url: "/data-collection", icon: <DatabaseIcon /> },
|
||||
{ title: "Настройки", url: "/settings", icon: <SettingsIcon /> },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
type LiveSidebarCounts = SidebarCountsDto & { greTunnels?: number }
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||
const evo = useEvoBGP()
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
const [liveCounts, setLiveCounts] = React.useState<LiveSidebarCounts | null>(null)
|
||||
|
||||
const mockBadges = React.useMemo(() => mockSidebarBadgesByUrl(), [])
|
||||
|
||||
React.useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
setMounted(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mode !== "live" || backendStatus !== true) {
|
||||
setLiveCounts(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
const load = async () => {
|
||||
try {
|
||||
const base = backendUrl.replace(/\/$/, "")
|
||||
const [cRes, gRes] = await Promise.all([
|
||||
fetch(`${base}/api/sidebar-counts`),
|
||||
fetch(`${base}/api/filters/gre-tunnels`),
|
||||
])
|
||||
if (cancelled) return
|
||||
if (!cRes.ok) {
|
||||
setLiveCounts(null)
|
||||
return
|
||||
}
|
||||
const cJson = (await cRes.json()) as SidebarCountsDto
|
||||
let greN = 0
|
||||
if (gRes.ok) {
|
||||
const gJson = (await gRes.json()) as { tunnels?: unknown[] }
|
||||
greN = (gJson.tunnels ?? []).length
|
||||
}
|
||||
setLiveCounts({ ...cJson, greTunnels: greN })
|
||||
} catch {
|
||||
if (!cancelled) setLiveCounts(null)
|
||||
}
|
||||
}
|
||||
void load()
|
||||
const id = window.setInterval(load, 120_000)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearInterval(id)
|
||||
}
|
||||
}, [mode, backendStatus, backendUrl])
|
||||
|
||||
const navGroups = React.useMemo((): NavGroup[] => {
|
||||
function badgeFor(url: string): string | undefined {
|
||||
if (mode !== "live") return mockBadges[url]
|
||||
if (backendStatus !== true) return undefined
|
||||
|
||||
if (url === "/domains" || url === "/ip-ranges" || url === "/asns") {
|
||||
if (!evo.enabled) return mockBadges[url]
|
||||
if (!evo.snapshot) return undefined
|
||||
if (url === "/domains") return formatSidebarBadgeCount(evo.snapshot.domains.length)
|
||||
if (url === "/ip-ranges") return formatSidebarBadgeCount(evo.snapshot.ipRanges.length)
|
||||
return formatSidebarBadgeCount(evo.snapshot.asns.length)
|
||||
}
|
||||
|
||||
if (!liveCounts) return undefined
|
||||
|
||||
if (url === "/servers") return formatSidebarBadgeCount(liveCounts.servers)
|
||||
if (url === "/filters") return formatSidebarBadgeCount(liveCounts.filterRules)
|
||||
if (url === "/uptime") return formatSidebarBadgeCount(liveCounts.monitoringItems)
|
||||
if (url === "/gre") return formatSidebarBadgeCount(liveCounts.greTunnels ?? 0)
|
||||
|
||||
if (url === "/wireguard" || url === "/containers" || url === "/certificates" || url === "/bgp") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
return navStructure.map((g) => ({
|
||||
label: g.label,
|
||||
items: g.items.map((it) => ({
|
||||
title: it.title,
|
||||
url: it.url,
|
||||
icon: it.icon,
|
||||
badge: badgeFor(it.url),
|
||||
})),
|
||||
}))
|
||||
}, [mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
||||
|
||||
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||||
|
||||
return (
|
||||
@@ -135,7 +228,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<span className="text-[10px] font-mono text-sidebar-foreground/40">v3.4.1</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Command palette trigger */}
|
||||
<CommandPaletteButton />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
@@ -143,16 +235,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<div className="flex items-center gap-2 px-2 py-2 group-data-[collapsible=icon]:justify-center">
|
||||
{/* Avatar */}
|
||||
<div className="size-7 rounded-full bg-gradient-to-br from-blue-500 to-violet-600 flex items-center justify-center text-white text-[10px] font-semibold shrink-0">
|
||||
АК
|
||||
</div>
|
||||
{/* Name / email */}
|
||||
<div className="flex flex-col leading-tight min-w-0 flex-1 group-data-[collapsible=icon]:hidden">
|
||||
<span className="text-[13px] font-medium text-sidebar-foreground truncate">а.коротаев</span>
|
||||
<span className="text-[11px] text-sidebar-foreground/50 truncate">admin@routerlists.io</span>
|
||||
</div>
|
||||
{/* Theme toggle — только после mount, иначе SSR и клиент расходятся (next-themes). */}
|
||||
{mounted ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
GlobeIcon, NetworkIcon, LayersIcon, TagIcon, ServerIcon, FilterIcon,
|
||||
ShieldIcon, ShieldCheckIcon, CableIcon, BoxIcon, BadgeCheckIcon,
|
||||
HardDriveIcon, RouteIcon, GitForkIcon, GitMergeIcon, ScanLineIcon,
|
||||
TerminalIcon, BellIcon, SettingsIcon,
|
||||
TerminalIcon, BellIcon, SettingsIcon, DatabaseIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
// ─── Command item definition ──────────────────────────────────────────────────
|
||||
@@ -52,8 +52,9 @@ const ALL_ITEMS: CommandItem[] = [
|
||||
{ id: "probes", title: "Диагностика", group: "Инструменты", url: "/probes", icon: <ScanLineIcon />, keywords: ["probe","ping","traceroute","диагностика"] },
|
||||
{ id: "terminal", title: "Терминал", group: "Инструменты", url: "/terminal", icon: <TerminalIcon />, keywords: ["terminal","cli","ssh","консоль"] },
|
||||
// Система
|
||||
{ id: "alerts", title: "Оповещения", group: "Система", url: "/alerts", icon: <BellIcon />, keywords: ["alert","notification","уведомление"] },
|
||||
{ id: "settings", title: "Настройки", group: "Система", url: "/settings", icon: <SettingsIcon />, keywords: ["settings","config","конфигурация"] },
|
||||
{ id: "alerts", title: "Оповещения", group: "Система", url: "/alerts", icon: <BellIcon />, keywords: ["alert","notification","уведомление"] },
|
||||
{ id: "data-collection", title: "Сбор данных", group: "Система", url: "/data-collection", icon: <DatabaseIcon />, keywords: ["scheduler","планировщик","коллектор","uptime","трафик","журнал","прогон"] },
|
||||
{ id: "settings", title: "Настройки", group: "Система", url: "/settings", icon: <SettingsIcon />, keywords: ["settings","config","конфигурация"] },
|
||||
]
|
||||
|
||||
// ─── Scoring / filtering ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface NavItem {
|
||||
title: string
|
||||
url: string
|
||||
icon?: React.ReactNode
|
||||
/** Если `undefined` — счётчик не показываем (нет данных или раздел без live-источника). */
|
||||
badge?: string
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ export function NavMain({ groups }: { groups: NavGroup[] }) {
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && (
|
||||
{item.badge != null && item.badge !== "" && (
|
||||
<span className="ml-auto text-[10.5px] font-mono tabular-nums text-sidebar-foreground/50 group-data-[collapsible=icon]:hidden">
|
||||
{item.badge}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user