"use client"

import { useState, useMemo, useEffect, useRef, useCallback } from "react"
import { PageHeader } from "@/components/page-header"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Flag } from "@/components/flag"
import { StatusDot } from "@/components/status-dot"
import { Sparkline } from "@/components/sparkline"
import { PING_PROBE_WARN_RTT_MS } from "@/lib/ping-probe"
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,
  TrashIcon, ArrowRightIcon,
  CpuIcon, HardDriveIcon, ThermometerIcon, ClockIcon,
  AlertCircleIcon, ServerIcon, PauseIcon, PlayIcon,
  ArrowUpIcon, ArrowDownIcon, ArrowUpDownIcon,
  ServerCrashIcon, ChevronUpIcon, DownloadIcon,
  PencilIcon,
  StarIcon,
  CopyIcon,
  TagIcon,
  CheckIcon,
} from "lucide-react"
import {
  Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter,
} from "@/components/ui/sheet"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

/** Звёздочка «на дашборде» для mock — общий ключ с `dashboard/page.tsx` */
const MOCK_DASH_STARS_LS = "mm:dashboard-probe-ids"
/** Сообщаем дашборду (та же вкладка) об изменении отметок / данных проб */
const UPTIME_PROBES_CHANGED = "mm:uptime-probes-changed"

function readMockDashboardStarIds(): Set<string> {
  if (typeof window === "undefined") return new Set()
  try {
    const raw = localStorage.getItem(MOCK_DASH_STARS_LS)
    const arr = raw ? (JSON.parse(raw) as unknown) : []
    return new Set(Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [])
  } catch {
    return new Set()
  }
}

function writeMockDashboardStarIds(ids: Set<string>) {
  localStorage.setItem(MOCK_DASH_STARS_LS, JSON.stringify([...ids]))
}

function mockProbesWithSavedStars(base: PingProbe[]): PingProbe[] {
  const stars = readMockDashboardStarIds()
  return base.map((p) => ({ ...p, showOnDashboard: stars.has(p.id) }))
}

function makeApiFetch(backendUrl: string) {
  return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
    return requestJson<T>(backendUrl, path, init)
  }
}

// ── helpers ────────────────────────────────────────────────────────────────────

function jitter(base: number, pct: number) {
  return Math.round(Math.max(1, base + (Math.random() - 0.5) * base * pct * 2))
}

function rttColor(rtt: number | null, loss: number): string {
  if (rtt === null || loss >= 100) return "text-[var(--status-offline-fg)]"
  if (loss > 1 || rtt > PING_PROBE_WARN_RTT_MS) return "text-[var(--status-degraded-fg)]"
  return "text-[var(--status-online-fg)]"
}

function probeSparkColor(status: PingProbe["status"]): string {
  return status === "down" ? "var(--status-offline)" : status === "warn" ? "var(--status-degraded)" : "var(--status-online)"
}

/** Развёрнутый график RTT под мини-спарклайном (та же серия `PingProbe.series`). */
function ProbePingRttDetailChart({
  series,
  status,
  probeName,
  target,
}: {
  series: number[]
  status: PingProbe["status"]
  probeName: string
  target: string
}) {
  const stroke = probeSparkColor(status)
  const data = series.map((v) => (v != null && Number.isFinite(v) ? Math.max(0, v) : 0))
  const valid = data.filter((v) => Number.isFinite(v))
  if (valid.length === 0) {
    return (
      <p className="text-xs text-muted-foreground">
        Нет числовых точек RTT для графика (проба «{probeName}» → {target}).
      </p>
    )
  }
  const chartPts = valid.length >= 2 ? data : [valid[0] ?? 0, valid[0] ?? 0]
  const W = 720
  const H = 168
  const pad = { l: 48, r: 14, t: 14, b: 36 }
  const iw = W - pad.l - pad.r
  const ih = H - pad.t - pad.b
  const maxVal = Math.max(...chartPts, 1)
  const minVal = Math.min(...chartPts)
  const span = Math.max(1, maxVal - minVal) * 1.08
  const y0 = minVal - (span - (maxVal - minVal)) / 2
  const y1 = y0 + span
  const xAt = (i: number) => pad.l + (chartPts.length <= 1 ? iw / 2 : (i / (chartPts.length - 1)) * iw)
  const yAt = (v: number) => pad.t + (1 - (v - y0) / span) * ih
  const lineD = chartPts
    .map((v, i) => `${i === 0 ? "M" : "L"}${xAt(i).toFixed(1)},${yAt(v).toFixed(1)}`)
    .join(" ")
  const areaD = `${lineD} L ${xAt(chartPts.length - 1).toFixed(1)},${pad.t + ih} L ${pad.l},${pad.t + ih} Z`
  const gridVals = [0, 0.25, 0.5, 0.75, 1]
  const fmt = (v: number) => `${Math.round(v)} мс`
  return (
    <div className="space-y-2">
      <div className="flex flex-wrap items-baseline justify-between gap-2">
        <p className="text-xs text-muted-foreground">
          <span className="font-medium text-foreground">{probeName}</span>
          <span className="font-mono ml-1.5">{target}</span>
        </p>
        <p className="text-[11px] text-muted-foreground">Ось X: старые замеры слева → новые справа · обзор ~1 ч</p>
      </div>
      <svg
        viewBox={`0 0 ${W} ${H}`}
        className="w-full max-w-[720px] h-[min(200px,42vw)] min-h-[140px]"
        style={{ display: "block" }}
        preserveAspectRatio="xMidYMid meet"
      >
        {gridVals.map((g, i) => {
          const y = pad.t + ih * (1 - g)
          return (
            <g key={i}>
              <line
                x1={pad.l}
                x2={W - pad.r}
                y1={y}
                y2={y}
                stroke="hsl(var(--border))"
                strokeDasharray={g === 0 ? "0" : "2 5"}
              />
              <text
                x={pad.l - 8}
                y={y + 4}
                textAnchor="end"
                fontSize="11"
                fill="hsl(var(--muted-foreground))"
                fontFamily="ui-monospace, monospace"
              >
                {fmt(y0 + span * g)}
              </text>
            </g>
          )
        })}
        <path d={areaD} style={{ fill: stroke, fillOpacity: 0.12 }} />
        <path
          d={lineD}
          fill="none"
          style={{ stroke }}
          strokeWidth="2"
          strokeLinejoin="round"
          strokeLinecap="round"
        />
        {[0, Math.floor((chartPts.length - 1) / 2), chartPts.length - 1]
          .filter((i, idx, a) => a.indexOf(i) === idx)
          .map((i) => (
            <text
              key={`x-${i}`}
              x={xAt(i)}
              y={H - 10}
              textAnchor="middle"
              fontSize="10"
              fill="hsl(var(--muted-foreground))"
              fontFamily="ui-monospace, monospace"
            >
              {i === chartPts.length - 1 ? "сейчас" : i === 0 ? "раньше" : "·"}
            </text>
          ))}
      </svg>
      <div className="flex flex-wrap gap-4 text-[11px] text-muted-foreground">
        <span>
          min <span className="font-mono text-foreground">{Math.round(minVal)}</span> мс
        </span>
        <span>
          max <span className="font-mono text-foreground">{Math.round(maxVal)}</span> мс
        </span>
        {valid.length < 2 && (
          <span className="text-amber-600 dark:text-amber-400">В ряду одна точка — линия для наглядности продублирована.</span>
        )}
      </div>
    </div>
  )
}

function probeGroupActionKey(srvId: string, group: { name: string; target: string }) {
  return `${srvId}\t${group.name}\t${group.target}`
}

// ── shared components ──────────────────────────────────────────────────────────

function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
  return (
    <button type="button" onClick={() => onChange(!checked)}
      className={cn(
        "relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors cursor-pointer",
        checked ? "bg-primary" : "bg-input",
      )}>
      <span className={cn(
        "pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform",
        checked ? "translate-x-4" : "translate-x-0",
      )} />
    </button>
  )
}

function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" }) {
  return (
    <span className={cn(
      "inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold border shrink-0",
      type === "home-router"
        ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
        : type === "jump-host"
          ? "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20"
          : "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
    )}>
      {type === "jump-host" ? "JH" : type === "home-router" ? "HR" : "EN"}
    </span>
  )
}

function StatChip({
  label, value, color, active, onClick,
}: {
  label: string; value: number; color?: "emerald" | "amber" | "red"; active?: boolean; onClick?: () => void
}) {
  return (
    <button
      onClick={onClick}
      className={cn(
        "flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-xs transition-all",
        active
          ? "bg-foreground text-background border-foreground"
          : "border-border hover:border-foreground/30 text-muted-foreground hover:text-foreground",
      )}>
      <span>{label}</span>
      <span className={cn(
        "font-semibold tabular-nums",
        !active && color === "emerald" ? "text-[var(--status-online-fg)]" :
        !active && color === "amber"   ? "text-[var(--status-degraded-fg)]" :
        !active && color === "red"     ? "text-[var(--status-offline-fg)]" : "",
      )}>{value}</span>
    </button>
  )
}

function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
  return (
    <div className="flex flex-col gap-1.5">
      <label className="text-xs font-medium">
        {label}
        {hint && <span className="font-normal text-muted-foreground ml-1">{hint}</span>}
      </label>
      {children}
    </div>
  )
}

/** Активный интерфейс RouterOS: не disabled и running */
function isActiveRouterOsInterface(i: { running?: boolean; disabled?: boolean }): boolean {
  return i.running === true && i.disabled !== true
}

function filterActiveInterfaces<T extends { running?: boolean; disabled?: boolean }>(list: T[]): T[] {
  return list.filter(isActiveRouterOsInterface)
}

function serverMatchesSearch(server: Server, raw: string): boolean {
  const q = raw.trim().toLowerCase()
  if (!q) return true
  const parts = [
    server.name,
    server.host,
    server.site,
    server.country,
    server.asn,
    server.type.replace(/-/g, " "),
    server.comment ?? "",
    server.model,
  ]
  return parts.some((p) => String(p).toLowerCase().includes(q))
}

type RouterInterfaceOption = {
  name: string
  running?: boolean
  disabled?: boolean
  addresses?: string[]
}

function interfaceOptionMatchesSearch(iface: RouterInterfaceOption, raw: string): boolean {
  const q = raw.trim().toLowerCase()
  if (!q) return true
  if (iface.name.toLowerCase().includes(q)) return true
  for (const addr of iface.addresses ?? []) {
    const t = addr.trim()
    const bare = t.includes("/") ? (t.split("/")[0] ?? "").trim() : t
    if (bare.toLowerCase().includes(q) || t.toLowerCase().includes(q)) return true
  }
  return false
}

function SegmentedControl<T extends string>({
  value,
  onChange,
  options,
}: {
  value: T
  onChange: (v: T) => void
  options: Array<{ value: T; label: string }>
}) {
  return (
    <div className="flex items-center gap-1 rounded-md border border-border bg-muted/40 p-0.5 w-fit">
      {options.map((option) => (
        <button
          key={option.value}
          type="button"
          onClick={() => onChange(option.value)}
          className={cn(
            "px-3 py-1 text-sm rounded transition-colors",
            value === option.value
              ? "bg-background text-foreground shadow-sm"
              : "text-muted-foreground hover:text-foreground",
          )}
        >
          {option.label}
        </button>
      ))}
    </div>
  )
}

function ServerPickerCards({
  options,
  selectedId,
  onSelect,
  blockedId,
}: {
  options: Server[]
  selectedId: string
  onSelect: (serverId: string) => void
  blockedId?: string
}) {
  const [query, setQuery] = useState("")
  const filtered = useMemo(() => {
    const q = query.trim()
    const base = options.filter((s) => serverMatchesSearch(s, q))
    if (!selectedId) return base
    const selected = options.find((s) => s.id === selectedId)
    if (!selected || base.some((s) => s.id === selectedId)) return base
    return [selected, ...base.filter((s) => s.id !== selectedId)]
  }, [options, query, selectedId])

  return (
    <div className="flex flex-col gap-2">
      <div className="relative">
        <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
        <Input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Поиск по имени, хосту, сайту…"
          className="h-8 pl-8 pr-8 text-sm"
          aria-label="Поиск сервера"
        />
        {query ? (
          <button
            type="button"
            onClick={() => setQuery("")}
            className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
            aria-label="Очистить поиск"
          >
            <XIcon className="size-3.5" />
          </button>
        ) : null}
      </div>
      <div className="flex flex-col gap-1.5 max-h-[240px] overflow-y-auto overflow-x-hidden pr-1">
        {filtered.length === 0 ? (
          <p className="text-xs text-muted-foreground px-1 py-2">Ничего не найдено</p>
        ) : (
          filtered.map((server) => {
            const isSelected = server.id === selectedId
            const isBlocked = blockedId === server.id
            return (
              <button
                key={server.id}
                type="button"
                onClick={() => onSelect(server.id)}
                disabled={isBlocked}
                className={cn(
                  "text-left rounded-lg border px-3 py-2.5 transition-all",
                  isSelected
                    ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                    : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
                  isBlocked && "opacity-40 cursor-not-allowed",
                )}
              >
                <div className="flex items-center gap-2">
                  <StatusDot status={server.status} />
                  <Flag code={server.country} className="shrink-0" />
                  <span className="font-mono text-xs font-semibold flex-1 truncate">{server.name}</span>
                  <TypeChip type={server.type} />
                  {isSelected && <StarIcon className="size-3.5 text-primary shrink-0 fill-current" />}
                </div>
                <div className="mt-1.5 flex items-center gap-2 text-[11px] text-muted-foreground font-mono">
                  <span>{server.site}</span>
                  <span className="text-muted-foreground/30">•</span>
                  <span className="truncate">{server.host}</span>
                </div>
                {!server.enabled && (
                  <p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400 leading-snug">
                    В инвентаре выключен — для ping всё равно можно выбрать, если бекенд достигает REST API.
                  </p>
                )}
              </button>
            )
          })
        )}
      </div>
    </div>
  )
}

function InterfacePickerCards({
  value,
  onChange,
  options,
  autoLabel,
  busy,
  disabled,
}: {
  value: string
  onChange: (v: string) => void
  options: RouterInterfaceOption[]
  autoLabel: string
  busy?: boolean
  disabled?: boolean
}) {
  const [query, setQuery] = useState("")
  const effectiveDisabled = disabled || busy

  const filtered = useMemo(() => {
    const q = query.trim()
    const base = options.filter((i) => interfaceOptionMatchesSearch(i, q))
    if (!value) return base
    const selected = options.find((i) => i.name === value)
    if (!selected || base.some((i) => i.name === value)) return base
    return [selected, ...base.filter((i) => i.name !== value)]
  }, [options, query, value])

  return (
    <div className="flex flex-col gap-2">
      <div className="relative">
        <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
        <Input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Поиск по имени или IP…"
          className="h-8 pl-8 pr-8 text-sm font-mono"
          disabled={effectiveDisabled}
          aria-label="Поиск интерфейса"
        />
        {query ? (
          <button
            type="button"
            onClick={() => setQuery("")}
            className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
            aria-label="Очистить поиск"
          >
            <XIcon className="size-3.5" />
          </button>
        ) : null}
      </div>

      <div className="flex flex-col gap-1.5">
        <button
          type="button"
          onClick={() => onChange("")}
          disabled={effectiveDisabled}
          className={cn(
            "text-left rounded-lg border px-3 py-2 transition-all",
            value === ""
              ? "border-primary bg-primary/5 ring-1 ring-primary/30"
              : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
            effectiveDisabled && "opacity-50 cursor-not-allowed",
          )}
        >
          <div className="flex items-center gap-2 text-xs">
            <span className={cn("size-1.5 rounded-full", value === "" ? "bg-primary" : "bg-muted-foreground/40")} />
            <span className="font-mono">{autoLabel}</span>
          </div>
        </button>

        <div className="flex flex-col gap-1.5 max-h-[180px] overflow-y-auto overflow-x-hidden pr-1">
          {filtered.length === 0 ? (
            <p className="text-xs text-muted-foreground px-1 py-2">Ничего не найдено</p>
          ) : (
            filtered.map((iface) => {
              const selected = value === iface.name
              const addrPreview = (iface.addresses ?? [])[0]
              return (
                <button
                  key={iface.name}
                  type="button"
                  onClick={() => onChange(iface.name)}
                  disabled={effectiveDisabled}
                  className={cn(
                    "text-left rounded-lg border px-3 py-2 transition-all",
                    selected
                      ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                      : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
                    effectiveDisabled && "opacity-50 cursor-not-allowed",
                  )}
                >
                  <div className="flex items-center gap-2 text-xs">
                    <span className="size-1.5 rounded-full bg-emerald-500 shrink-0" />
                    <span className="font-mono font-medium truncate">{iface.name}</span>
                  </div>
                  {addrPreview ? (
                    <p className="mt-1 pl-3.5 text-[10px] font-mono text-muted-foreground truncate">
                      {addrPreview}
                    </p>
                  ) : null}
                </button>
              )
            })
          )}
        </div>
      </div>
    </div>
  )
}

const LINKED_FILTER_NONE = "—"

function LinkedFilterPickerCards({
  value,
  onChange,
  items,
}: {
  value: string
  onChange: (v: string) => void
  items: Filter[]
}) {
  const [query, setQuery] = useState("")
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase()
    let base = !q
      ? items
      : items.filter((f) =>
          f.name.toLowerCase().includes(q) ||
          f.id.toLowerCase().includes(q) ||
          f.gateway.toLowerCase().includes(q) ||
          (f.communities ?? []).some((c) => c.toLowerCase().includes(q)),
        )
    if (value && value !== LINKED_FILTER_NONE) {
      const sel = items.find((f) => f.name === value)
      if (sel && !base.some((f) => f.name === value)) {
        base = [sel, ...base.filter((f) => f.name !== value)]
      }
    }
    return base
  }, [items, query, value])

  const isNone = value === LINKED_FILTER_NONE || !value

  return (
    <div className="flex flex-col gap-2">
      <div className="relative">
        <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
        <Input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Поиск по имени, community, gateway…"
          className="h-8 pl-8 pr-8 text-sm"
          aria-label="Поиск фильтра"
        />
        {query ? (
          <button
            type="button"
            onClick={() => setQuery("")}
            className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
            aria-label="Очистить поиск"
          >
            <XIcon className="size-3.5" />
          </button>
        ) : null}
      </div>

      <div className="flex flex-col gap-1.5 max-h-[220px] overflow-y-auto overflow-x-hidden pr-1">
        <button
          type="button"
          onClick={() => onChange(LINKED_FILTER_NONE)}
          className={cn(
            "text-left rounded-lg border px-3 py-2.5 transition-all",
            isNone
              ? "border-primary bg-primary/5 ring-1 ring-primary/30"
              : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
          )}
        >
          <div className="flex items-center gap-2">
            <span className="text-muted-foreground"><TagIcon className="size-3.5" /></span>
            <span className="text-sm font-medium">Без привязки</span>
            {isNone && <CheckIcon className="size-3.5 text-primary shrink-0 ml-auto" />}
          </div>
          <p className="mt-1 text-[11px] text-muted-foreground pl-5">Не связывать пробу с правилом фильтрации</p>
        </button>

        {filtered.length === 0 ? (
          query.trim() ? (
            <p className="text-xs text-muted-foreground px-1 py-2">Ничего не найдено</p>
          ) : null
        ) : (
          filtered.map((f) => {
            const selected = value === f.name
            return (
              <button
                key={f.id}
                type="button"
                onClick={() => onChange(f.name)}
                className={cn(
                  "text-left rounded-lg border px-3 py-2.5 transition-all",
                  selected
                    ? "border-primary bg-primary/5 ring-1 ring-primary/30"
                    : "border-border hover:border-muted-foreground/40 hover:bg-muted/40",
                )}
              >
                <div className="flex items-center gap-2">
                  <TagIcon className="size-3.5 text-muted-foreground shrink-0" />
                  <span className="font-mono text-xs font-semibold flex-1 truncate">{f.name}</span>
                  {selected && <CheckIcon className="size-3.5 text-primary shrink-0" />}
                </div>
                <div className="mt-1.5 flex flex-wrap gap-x-2 gap-y-0.5 text-[11px] text-muted-foreground font-mono pl-5">
                  <span>{f.domains} дом.</span>
                  <span className="text-muted-foreground/40">·</span>
                  <span>{f.ips} IP</span>
                  <span className="text-muted-foreground/40">·</span>
                  <span className="truncate max-w-[140px]" title={f.gateway}>gw {f.gateway}</span>
                </div>
              </button>
            )
          })
        )}
      </div>
    </div>
  )
}

// ── Resource monitoring types + helpers ───────────────────────────────────────

interface ServerResource {
  serverId: string
  /** false — в выбранном окне нет сэмплов ресурсов (не подменяем нулями «реальные» 0 %) */
  hasData?: boolean
  cpu: number
  cpuHistory: number[]
  ramUsed: number   // MB
  ramTotal: number  // MB
  hddUsed: number   // MB
  hddTotal: number  // MB
  uptimeSeconds: number
  boardName: string
  temp?: number     // °C
}

interface BackendServer {
  id: number
  name: string
  host: string
  type: "jump-host" | "exit-node" | "home-router"
  site: string
  country: string
  enabled: boolean
  status: "online" | "offline" | null
  latency: number | null
  os: string | null
}

function mapBackendServersToServers(data: BackendServer[]): Server[] {
  return data.map((s) => ({
    id: String(s.id),
    name: s.name || s.host,
    host: s.host,
    model: "—",
    os: s.os ?? "—",
    site: s.site || "—",
    country: s.country || "UN",
    asn: "",
    type: s.type,
    enabled: s.enabled,
    status: (s.status ?? "offline") as Server["status"],
    latency: s.latency != null ? Math.round(s.latency) : null,
    sessions: 0,
  }))
}

interface SpeedTestRun {
  id: string
  startedAt: number
  srcServerId: string
  dstServerId: string
  srcInterface?: string
  dstInterface?: string
  protocol: "tcp" | "udp"
  direction: "transmit" | "receive" | "both"
  durationSec: number
  txAvgMbps: number
  rxAvgMbps: number
  status: "running" | "done" | "error"
  command: string
  lines: string[]
  afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null
  srcAddress?: string | null
  dstAddress?: string | null
  srcInterfaceAddress?: string | null
  dstInterfaceAddress?: string | null
}

interface SpeedProbeRow {
  id: string
  srcServerId: string
  dstServerId: string
  srcInterface: string
  dstInterface: string
  protocol: "tcp" | "udp"
  direction: "transmit" | "receive" | "both"
  durationSec: string
  enabled: boolean
  lastRunAt?: string | null
  lastTxAvgMbps?: number | null
  lastRxAvgMbps?: number | null
  lastStatus?: "done" | "error" | null
  lastError?: string | null
  lastPingRttMs?: number | null
  lastPingLossPct?: number | null
  lastPingAt?: string | null
  lastPingError?: string | null
}

/** Сервер есть в БД speed-проб, но удалён из каталога — показываем группу без ломания списка */
function orphanSpeedSourceStub(id: string): Server {
  return {
    id,
    name: `Нет в каталоге (#${id})`,
    host: "—",
    model: "—",
    os: "—",
    site: "—",
    country: "UN",
    asn: "",
    type: "home-router",
    enabled: false,
    status: "offline",
    latency: null,
    sessions: 0,
  }
}

function stripIpCidr(addr: string): string {
  const t = addr.trim()
  if (!t) return ""
  return t.includes("/") ? (t.split("/")[0] ?? "").trim() : t
}

function resolveSpeedProbeDstHost(
  sp: SpeedProbeRow,
  servers: Server[],
  ifaces: Record<string, Array<{ name: string; addresses: string[] }>>,
): string {
  const dst = servers.find((s) => s.id === sp.dstServerId)
  if (!dst) return ""
  const iface = sp.dstInterface?.trim()
  if (iface) {
    const row = ifaces[sp.dstServerId]?.find((i) => i.name === iface)
    const raw = row?.addresses?.[0]
    const ip = raw ? stripIpCidr(raw) : ""
    return (ip || dst.host).trim().toLowerCase()
  }
  return dst.host.trim().toLowerCase()
}

function findLinkedSpeedProbe(
  ping: PingProbe,
  speedList: SpeedProbeRow[],
  servers: Server[],
  ifaces: Record<string, Array<{ name: string; addresses: string[] }>>,
): SpeedProbeRow | undefined {
  const t = ping.target.trim().toLowerCase()
  if (!t) return undefined
  return speedList.find((sp) => {
    if (sp.srcServerId !== ping.srcServerId) return false
    if ((sp.srcInterface ?? "").trim() !== (ping.srcInterface ?? "").trim()) return false
    const resolved = resolveSpeedProbeDstHost(sp, servers, ifaces)
    const dst = servers.find((s) => s.id === sp.dstServerId)
    const host = dst?.host.trim().toLowerCase() ?? ""
    return (resolved.length > 0 && t === resolved) || t === host
  })
}

function fmtMB(mb: number): string {
  if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} ГБ`
  return `${mb.toFixed(1)} МБ`
}

function fmtUptime(sec: number): string {
  const d = Math.floor(sec / 86400)
  const h = Math.floor((sec % 86400) / 3600)
  const m = Math.floor((sec % 3600) / 60)
  if (d > 0) return `${d}д ${h}ч`
  if (h > 0) return `${h}ч ${m}м`
  return `${m}м`
}

function resPctColor(pct: number, warn = 70, crit = 85): string {
  if (pct >= crit) return "text-red-600 dark:text-red-400"
  if (pct >= warn) return "text-amber-600 dark:text-amber-400"
  return "text-emerald-600 dark:text-emerald-400"
}

function resBarColor(pct: number, warn = 70, crit = 85): string {
  if (pct >= crit) return "bg-red-500"
  if (pct >= warn) return "bg-amber-500"
  return "bg-emerald-500"
}

const BOARD_MAP: Record<string, string> = {
  "jump-host":   "RB5009UG+S+IN",
  "exit-node":   "RB4011iGS+RM",
  "home-router": "hAP ax²",
}

// Deterministic pseudo-random initial resource values per server
const INIT_RESOURCES: ServerResource[] = mockServers.map(s => {
  const h = s.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0)
  const cpu      = 4 + (h % 68)
  const ramTotal = s.type === "jump-host" ? 8192 : s.type === "exit-node" ? 4096 : 1024
  const hddTotal = s.type === "home-router" ? 2048 : 16384
  const ramPct   = 12 + (h % 72)
  const hddPct   = 8  + (h % 75)
  return {
    serverId:      s.id,
    cpu,
    cpuHistory:    Array.from({ length: 40 }, (_, i) =>
      Math.max(1, Math.min(99, cpu + Math.round(Math.sin(i * 0.7 + h * 0.1) * 15)))
    ),
    ramUsed:       Math.round(ramTotal * ramPct / 100),
    ramTotal,
    hddUsed:       Math.round(hddTotal * hddPct / 100),
    hddTotal,
    uptimeSeconds: (1 + h % 200) * 86400 + (h % 24) * 3600 + (h % 60) * 60,
    boardName:     BOARD_MAP[s.type] ?? "RouterBOARD",
    temp:          s.type !== "home-router" ? 34 + (h % 32) : undefined,
    hasData:       true,
  }
})

// ── MiniBar ───────────────────────────────────────────────────────────────────

function MiniBar({ pct, warn = 70, crit = 85, className }: {
  pct: number; warn?: number; crit?: number; className?: string
}) {
  return (
    <div className={cn("h-1.5 rounded-full bg-muted overflow-hidden", className)}>
      <div
        className={cn("h-full rounded-full transition-all duration-700", resBarColor(pct, warn, crit))}
        style={{ width: `${Math.min(100, Math.max(0, pct))}%` }}
      />
    </div>
  )
}

// ── Sort indicator ─────────────────────────────────────────────────────────────

type ResSortKey = "name" | "cpu" | "ram" | "hdd" | "uptime" | "temp"

function SortIcon({ k, sortKey, sortAsc }: { k: ResSortKey; sortKey: ResSortKey; sortAsc: boolean }) {
  if (sortKey !== k) return <ArrowUpDownIcon className="size-3 opacity-25 ml-0.5" />
  return sortAsc
    ? <ArrowUpIcon   className="size-3 ml-0.5 text-foreground" />
    : <ArrowDownIcon className="size-3 ml-0.5 text-foreground" />
}

// ── ResourcesTab ──────────────────────────────────────────────────────────────

type ResTypeFilter = "all" | "jump-host" | "exit-node" | "home-router"

function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerResource[]; serversList: Server[]; liveApi?: boolean }) {
  const [sortKey,    setSortKey]    = useState<ResSortKey>("name")
  const [sortAsc,    setSortAsc]    = useState(true)
  const [resSearch,  setResSearch]  = useState("")
  const [typeFilter, setTypeFilter] = useState<ResTypeFilter>("all")

  const rows = useMemo(() => resources.map((r) => {
    const hasData = r.hasData !== false
    const ramPct = hasData && r.ramTotal > 0 ? Math.round(r.ramUsed / r.ramTotal * 100) : 0
    const hddPct = hasData && r.hddTotal > 0 ? Math.round(r.hddUsed / r.hddTotal * 100) : 0
    return {
      ...r,
      hasData,
      server: serversList.find(s => s.id === r.serverId),
      ramPct,
      hddPct,
    }
  }).filter(r => r.server !== undefined), [resources, serversList])

  // KPI aggregates (только серверы с реальными сэмплами за окно)
  const onlineWithSamples = rows.filter(r => r.server!.status === "online" && r.hasData)
  const avgCpu  = onlineWithSamples.length ? Math.round(onlineWithSamples.reduce((s, r) => s + r.cpu, 0) / onlineWithSamples.length) : 0
  const avgRam  = onlineWithSamples.length ? Math.round(onlineWithSamples.reduce((s, r) => s + r.ramPct, 0) / onlineWithSamples.length) : 0
  const highCpu = rows.filter(r => r.server!.status === "online" && r.hasData && r.cpu >= 85).length
  const highRam = rows.filter(r => r.server!.status === "online" && r.hasData && r.ramPct >= 85).length
  const highHdd = rows.filter(r => r.server!.status === "online" && r.hasData && r.hddPct >= 85).length

  // Alerts
  const alerts = useMemo(() =>
    rows.filter(r => r.server!.status === "online" && r.hasData && (r.cpu >= 85 || r.ramPct >= 85 || r.hddPct >= 85 || (r.temp ?? 0) >= 70)),
    [rows],
  )

  // Filtered + sorted
  const visible = useMemo(() => {
    let list = rows
    if (typeFilter !== "all") list = list.filter(r => r.server!.type === typeFilter)
    if (resSearch.trim()) {
      const q = resSearch.toLowerCase()
      list = list.filter(r =>
        r.server!.name.toLowerCase().includes(q) ||
        r.server!.site.toLowerCase().includes(q) ||
        r.boardName.toLowerCase().includes(q)
      )
    }
    list = [...list].sort((a, b) => {
      let diff = 0
      switch (sortKey) {
        case "name":   diff = a.server!.name.localeCompare(b.server!.name); break
        case "cpu":    diff = a.cpu    - b.cpu;    break
        case "ram":    diff = a.ramPct - b.ramPct; break
        case "hdd":    diff = a.hddPct - b.hddPct; break
        case "uptime": diff = a.uptimeSeconds - b.uptimeSeconds; break
        case "temp":   diff = (a.temp ?? -1) - (b.temp ?? -1); break
      }
      return sortAsc ? diff : -diff
    })
    return list
  }, [rows, typeFilter, resSearch, sortKey, sortAsc])

  function toggleSort(k: ResSortKey) {
    if (sortKey === k) setSortAsc(v => !v)
    else { setSortKey(k); setSortAsc(false) } // default desc for metrics
  }

  function exportCsv() {
    const header = ["Сервер", "Тип", "Площадка", "CPU %", "RAM %", "RAM использ.", "RAM всего", "HDD %", "HDD использ.", "HDD всего", "Uptime", "Температура °C", "RouterOS"]
    const rowsCsv = visible.map(r => {
      const s = r.server!
      return [s.name, s.type, s.site, r.cpu, r.ramPct, fmtMB(r.ramUsed), fmtMB(r.ramTotal),
              r.hddPct, fmtMB(r.hddUsed), fmtMB(r.hddTotal), fmtUptime(r.uptimeSeconds),
              r.temp ?? "", s.os].join(",")
    })
    const csv = [header.join(","), ...rowsCsv].join("\n")
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" })
    const url = URL.createObjectURL(blob)
    const a = document.createElement("a")
    a.href = url; a.download = `resources-${new Date().toISOString().slice(0,10)}.csv`; a.click()
    URL.revokeObjectURL(url)
  }

  const typeOpts: { value: ResTypeFilter; label: string }[] = [
    { value: "all",         label: "Все"         },
    { value: "jump-host",   label: "JumpHost"    },
    { value: "exit-node",   label: "Exit Node"   },
    { value: "home-router", label: "Home Router" },
  ]

  return (
    <div className="p-6 flex flex-col gap-4">

      {/* ── Alert banner ──────────────────────────────────────────────────── */}
      {alerts.length > 0 && (
        <div className="flex items-start gap-3 rounded-lg border border-red-500/30 bg-red-500/5 px-4 py-3">
          <ServerCrashIcon className="size-4 text-red-500 shrink-0 mt-0.5" />
          <div className="flex-1 min-w-0">
            <p className="text-xs font-semibold text-red-600 dark:text-red-400 mb-1">
              {alerts.length} {alerts.length === 1 ? "сервер требует внимания" : "сервера требуют внимания"}
            </p>
            <div className="flex flex-wrap gap-1.5">
              {alerts.map(r => {
                const s = r.server!
                const issues: string[] = []
                if (r.cpu >= 85)    issues.push(`CPU ${r.cpu}%`)
                if (r.ramPct >= 85) issues.push(`RAM ${r.ramPct}%`)
                if (r.hddPct >= 85) issues.push(`HDD ${r.hddPct}%`)
                if ((r.temp ?? 0) >= 70) issues.push(`${r.temp}°C`)
                return (
                  <span key={r.serverId} className="inline-flex items-center gap-1 text-[11px] font-mono
                    rounded border border-red-500/30 bg-red-500/10 px-2 py-0.5 text-red-400">
                    <Flag code={s.country} size={10} />
                    {s.name} — {issues.join(", ")}
                  </span>
                )
              })}
            </div>
          </div>
        </div>
      )}

      {/* ── KPI summary ───────────────────────────────────────────────────── */}
      <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
        {[
          { icon: <ServerIcon      className="size-4 text-muted-foreground" />, label: String(rows.length), sub: "серверов всего",  color: "text-foreground" },
          { icon: <CpuIcon         className="size-4" />,                       label: `${avgCpu}%`,         sub: "средний CPU",     color: resPctColor(avgCpu) },
          { icon: <HardDriveIcon   className="size-4" />,                       label: `${avgRam}%`,         sub: "средний RAM",     color: resPctColor(avgRam) },
          { icon: <AlertCircleIcon className="size-4" />,                       label: String(highCpu),      sub: "CPU > 85%",       color: highCpu > 0 ? "text-red-500"    : "text-muted-foreground" },
          { icon: <AlertCircleIcon className="size-4" />,                       label: String(highRam),      sub: "RAM > 85%",       color: highRam > 0 ? "text-red-500"    : "text-muted-foreground" },
          { icon: <AlertCircleIcon className="size-4" />,                       label: String(highHdd),      sub: "Диск > 85%",      color: highHdd > 0 ? "text-amber-500"  : "text-muted-foreground" },
        ].map(kpi => (
          <Card key={kpi.sub}>
            <CardContent className="px-4 py-3 flex items-start gap-3">
              <div className={cn("mt-0.5 shrink-0", kpi.color)}>{kpi.icon}</div>
              <div className="min-w-0">
                <p className={cn("text-xl font-semibold tabular-nums leading-tight", kpi.color)}>{kpi.label}</p>
                <p className="text-[11px] text-muted-foreground mt-0.5">{kpi.sub}</p>
              </div>
            </CardContent>
          </Card>
        ))}
      </div>

      {/* ── Toolbar ───────────────────────────────────────────────────────── */}
      <div className="flex items-center gap-3 flex-wrap">
        {/* Type filter */}
        <div className="flex items-center gap-0.5 rounded-md border border-border bg-muted/40 p-0.5 shrink-0">
          {typeOpts.map(o => (
            <button key={o.value} onClick={() => setTypeFilter(o.value)}
              className={cn(
                "px-2.5 py-1 text-xs rounded transition-colors whitespace-nowrap",
                typeFilter === o.value
                  ? "bg-background text-foreground shadow-sm"
                  : "text-muted-foreground hover:text-foreground",
              )}>
              {o.label}
            </button>
          ))}
        </div>

        {/* Search */}
        <div className="relative min-w-[180px] flex-1 max-w-xs">
          <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
          <Input className="pl-8 h-8 text-sm" placeholder="Поиск по имени, площадке…"
            value={resSearch} onChange={e => setResSearch(e.target.value)} />
          {resSearch && (
            <button onClick={() => setResSearch("")}
              className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
              <XIcon className="size-3.5" />
            </button>
          )}
        </div>

        <span className="text-xs text-muted-foreground ml-auto shrink-0">
          {visible.length} из {rows.length} серверов
        </span>

        {/* Export CSV */}
        <Button variant="outline" size="sm" className="h-8 gap-1.5 shrink-0" onClick={exportCsv}>
          <DownloadIcon className="size-3.5" />CSV
        </Button>
      </div>

      {/* ── Table ─────────────────────────────────────────────────────────── */}
      <Card className="overflow-hidden">
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead>
              <tr className="border-b bg-muted/30 text-xs text-muted-foreground font-medium">

                {/* Sortable: name */}
                <th className="text-left px-5 py-3 cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("name")}>
                  <span className="flex items-center gap-0.5">
                    Сервер <SortIcon k="name" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>

                <th className="text-left px-4 py-3 hidden md:table-cell whitespace-nowrap">Модель · ROS</th>

                {/* Sortable: cpu */}
                <th className="text-left px-4 py-3 min-w-[160px] cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("cpu")}>
                  <span className="flex items-center gap-1.5">
                    <CpuIcon className="size-3.5" />CPU
                    <SortIcon k="cpu" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>

                {/* Sortable: ram */}
                <th className="text-left px-4 py-3 min-w-[175px] cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("ram")}>
                  <span className="flex items-center gap-1.5">
                    <HardDriveIcon className="size-3.5" />RAM
                    <SortIcon k="ram" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>

                {/* Sortable: hdd */}
                <th className="text-left px-4 py-3 min-w-[175px] cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("hdd")}>
                  <span className="flex items-center gap-1.5">
                    <HardDriveIcon className="size-3.5" />Диск
                    <SortIcon k="hdd" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>

                {/* Sortable: uptime */}
                <th className="text-left px-4 py-3 cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("uptime")}>
                  <span className="flex items-center gap-1.5">
                    <ClockIcon className="size-3.5" />Uptime
                    <SortIcon k="uptime" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>

                {/* Sortable: temp */}
                <th className="text-left px-4 py-3 cursor-pointer hover:text-foreground transition-colors select-none"
                  onClick={() => toggleSort("temp")}>
                  <span className="flex items-center gap-1.5">
                    <ThermometerIcon className="size-3.5" />°C
                    <SortIcon k="temp" sortKey={sortKey} sortAsc={sortAsc} />
                  </span>
                </th>
              </tr>
            </thead>
            <tbody className="divide-y divide-border">
              {visible.length === 0 && (
                <tr>
                  <td colSpan={7} className="text-center text-sm text-muted-foreground py-12">
                    <SearchIcon className="size-6 mx-auto mb-2 opacity-20" />
                    Ничего не найдено
                  </td>
                </tr>
              )}
              {visible.map(r => {
                const srv = r.server!
                const offline = srv.status !== "online"
                const hasSamples = r.hasData !== false
                const noMetrics = offline || !hasSamples
                const isCrit  = !noMetrics && (r.cpu >= 85 || r.ramPct >= 85 || r.hddPct >= 85 || (r.temp ?? 0) >= 70)
                const cpuColor = r.cpu >= 85 ? "hsl(0 84% 60%)" : r.cpu >= 70 ? "hsl(38 92% 50%)" : "hsl(142 76% 36%)"
                return (
                  <tr key={r.serverId} className={cn(
                    "hover:bg-muted/30 transition-colors",
                    offline && "opacity-50",
                    isCrit  && "bg-red-500/3",
                  )}>

                    {/* Server */}
                    <td className="px-5 py-3">
                      <div className="flex items-center gap-2 flex-wrap">
                        {isCrit && <AlertCircleIcon className="size-3.5 text-red-500 shrink-0" />}
                        {!isCrit && <StatusDot status={srv.status} pulse={!offline} />}
                        <Flag code={srv.country} size={16} />
                        <span className="font-mono font-semibold">{srv.name}</span>
                        <TypeChip type={srv.type} />
                        <span className="text-xs text-muted-foreground hidden xl:inline">{srv.site}</span>
                        {!offline && r.hasData === false && (
                          <span className="text-[10px] rounded border border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400 px-1.5 py-0.5">
                            нет данных
                          </span>
                        )}
                      </div>
                    </td>

                    {/* Board + ROS */}
                    <td className="px-4 py-3 hidden md:table-cell">
                      <div className="flex flex-col leading-tight">
                        <span className="font-mono text-xs text-muted-foreground">{hasSamples ? r.boardName : "—"}</span>
                        <span className="text-[10px] text-muted-foreground/50">{srv.os}</span>
                      </div>
                    </td>

                    {/* CPU */}
                    <td className="px-4 py-3">
                      {noMetrics
                        ? <span className="text-xs text-muted-foreground/30">{offline ? "—" : "нет опроса"}</span>
                        : (
                          <div className="flex flex-col gap-1.5 min-w-[140px]">
                            <div className="flex items-center gap-2">
                              <span className={cn("font-mono text-sm font-semibold tabular-nums w-10 shrink-0", resPctColor(r.cpu))}>
                                {r.cpu}%
                              </span>
                              <MiniBar pct={r.cpu} className="flex-1" />
                            </div>
                            <Sparkline data={r.cpuHistory} width={120} height={18} color={cpuColor} filled />
                          </div>
                        )}
                    </td>

                    {/* RAM */}
                    <td className="px-4 py-3">
                      {noMetrics
                        ? <span className="text-xs text-muted-foreground/30">{offline ? "—" : "нет опроса"}</span>
                        : (
                          <div className="flex flex-col gap-1.5 min-w-[155px]">
                            <div className="flex items-center justify-between text-xs">
                              <span className={cn("font-mono font-semibold", resPctColor(r.ramPct))}>{r.ramPct}%</span>
                              <span className="text-muted-foreground/60 font-mono text-[10px]">
                                {fmtMB(r.ramUsed)}/{fmtMB(r.ramTotal)}
                              </span>
                            </div>
                            <MiniBar pct={r.ramPct} />
                          </div>
                        )}
                    </td>

                    {/* HDD */}
                    <td className="px-4 py-3">
                      {noMetrics
                        ? <span className="text-xs text-muted-foreground/30">{offline ? "—" : "нет опроса"}</span>
                        : (
                          <div className="flex flex-col gap-1.5 min-w-[155px]">
                            <div className="flex items-center justify-between text-xs">
                              <span className={cn("font-mono font-semibold", resPctColor(r.hddPct))}>{r.hddPct}%</span>
                              <span className="text-muted-foreground/60 font-mono text-[10px]">
                                {fmtMB(r.hddUsed)}/{fmtMB(r.hddTotal)}
                              </span>
                            </div>
                            <MiniBar pct={r.hddPct} />
                          </div>
                        )}
                    </td>

                    {/* Uptime */}
                    <td className="px-4 py-3">
                      <span className="font-mono text-xs text-muted-foreground">
                        {noMetrics ? (offline ? "—" : "—") : fmtUptime(r.uptimeSeconds)}
                      </span>
                    </td>

                    {/* Temp */}
                    <td className="px-4 py-3">
                      {r.temp !== undefined && !noMetrics ? (
                        <span className={cn("font-mono text-sm font-semibold tabular-nums",
                          r.temp >= 70 ? "text-red-600 dark:text-red-400"
                          : r.temp >= 55 ? "text-amber-600 dark:text-amber-400"
                          : "text-muted-foreground",
                        )}>
                          {r.temp}°C
                        </span>
                      ) : (
                        <span className="text-muted-foreground/30 text-xs">—</span>
                      )}
                    </td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>
      </Card>

      <p className="text-xs text-muted-foreground/40 text-center">
        {liveApi
          ? "Автообновление каждые 5 сек (и кнопка «Обновить») · /system/resource via RouterOS REST API · backend"
          : "Обновление каждые 5 сек · /system/resource via RouterOS REST API · demo-режим"}
      </p>
    </div>
  )
}

// ── page ───────────────────────────────────────────────────────────────────────

export default function UptimePage() {
  const [allServers, setAllServers] = useState<Server[]>(mockServers)
  const { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
  /** При mode=live всегда ходим на backend. Нельзя требовать backendStatus===true: до ответа /health там undefined — иначе обзор/«Обновить» молчат. */
  const liveApi = mode === "live"
  const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])

  const [tab,          setTab]          = useState<"probes" | "resources" | "speed">("probes")
  const [probes,       setProbes]       = useState<PingProbe[]>(INIT_PROBES)
  const [resources,    setResources]    = useState<ServerResource[]>(INIT_RESOURCES)
  const [isPaused,     setIsPaused]     = useState(false)
  const [search,       setSearch]       = useState("")
  const [statusFilter, setStatusFilter] = useState<"all" | "up" | "warn" | "down">("all")
  const [serverFilter, setServerFilter] = useState<string[]>([])
  const [collapsed,    setCollapsed]    = useState<Set<string>>(new Set())
  const [sheetOpen,    setSheetOpen]    = useState(false)
  const [opError,      setOpError]      = useState<string | null>(null)
  const [uptimeRefreshBusy, setUptimeRefreshBusy] = useState(false)
  /** Ключ — probeGroupActionKey: ручной ping группы «сервер + назначение». */
  const [probeGroupPingBusy, setProbeGroupPingBusy] = useState<Record<string, boolean>>({})
  /** Раскрытый подробный график RTT по id пробы */
  const [probeRttChartOpen, setProbeRttChartOpen] = useState<Record<string, boolean>>({})
  const [speedBusy, setSpeedBusy] = useState(false)
  const [speedError, setSpeedError] = useState<string | null>(null)
  const [speedRuns, setSpeedRuns] = useState<SpeedTestRun[]>([])
  const [speedProbes, setSpeedProbes] = useState<SpeedProbeRow[]>([])
  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)
  const [editingSpeedProbeId, setEditingSpeedProbeId] = useState<string | null>(null)
  const [speedDraft, setSpeedDraft] = useState<SpeedProbeRow>({
    id: "",
    srcServerId: "",
    dstServerId: "",
    srcInterface: "",
    dstInterface: "",
    protocol: "tcp",
    direction: "both",
    durationSec: "10",
    enabled: true,
  })

  useEffect(() => {
    if (!liveApi) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setAllServers(mockServers)
      return
    }
    /** В live каталог серверов подгружается вместе с overview (см. loadLiveOverview), иначе после «Серверы» строки ресурсов пропадают из-за .filter(r => r.server). */
  }, [liveApi])

  useEffect(() => {
    if (liveApi) return
    queueMicrotask(() => setProbes(mockProbesWithSavedStars(INIT_PROBES)))
  }, [liveApi])

  const isPausedRef = useRef(isPaused)
  useEffect(() => { isPausedRef.current = isPaused }, [isPaused])

  /** Сбрасывает ответы устаревших GET /overview (гонка: ответ приходит после клика по ★ и затирает showOnDashboard). */
  const overviewReqRef = useRef(0)

  const loadLiveOverview = useCallback(async () => {
    if (!liveApi) return
    const myReq = ++overviewReqRef.current
    setOpError(null)
    try {
      const [serverRows, data] = await Promise.all([
        apiFetch<BackendServer[]>("/api/servers"),
        apiFetch<{ probes: PingProbe[]; resources: ServerResource[] }>("/api/uptime/overview?range=1h"),
      ])
      if (myReq !== overviewReqRef.current) return
      setAllServers(mapBackendServersToServers(serverRows))
      setProbes(data.probes)
      setResources(data.resources)
    } catch (e) {
      if (myReq !== overviewReqRef.current) return
      setOpError(e instanceof Error ? e.message : "Не удалось загрузить uptime")
    }
  }, [apiFetch, liveApi])

  const reloadSpeedData = useCallback(async () => {
    if (!liveApi) return
    type RunRow = {
      id: string
      srcServerId: string
      dstServerId: string
      srcInterface: string
      dstInterface: string
      protocol: "tcp" | "udp"
      direction: "transmit" | "receive" | "both"
      durationSec: number
      txAvgMbps: number
      rxAvgMbps: number
      status: "done" | "error"
      error?: string | null
      srcAddress?: string | null
      dstAddress?: string | null
      srcInterfaceAddress?: string | null
      dstInterfaceAddress?: string | null
      afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null
      createdAt: string
    }
    try {
      const [sp, runsRes] = await Promise.all([
        apiFetch<{ probes: SpeedProbeRow[] }>("/api/uptime/speed-probes"),
        apiFetch<{ runs: RunRow[] }>("/api/uptime/speed-test/runs"),
      ])
      setSpeedProbes(sp.probes ?? [])
      const rows = (runsRes.runs ?? []).map((r) => ({
        id: r.id,
        startedAt: Date.parse(r.createdAt),
        srcServerId: r.srcServerId,
        dstServerId: r.dstServerId,
        srcInterface: r.srcInterface || undefined,
        dstInterface: r.dstInterface || undefined,
        protocol: r.protocol,
        direction: r.direction,
        durationSec: r.durationSec,
        txAvgMbps: Math.round(r.txAvgMbps ?? 0),
        rxAvgMbps: Math.round(r.rxAvgMbps ?? 0),
        status: (r.status === "error" ? "error" : "done") as "error" | "done",
        command: "",
        lines: r.error ? [`status: error`, r.error] : [],
        afterBtPing: r.afterBtPing ?? null,
        srcAddress: r.srcAddress ?? null,
        dstAddress: r.dstAddress ?? null,
        srcInterfaceAddress: r.srcInterfaceAddress ?? null,
        dstInterfaceAddress: r.dstInterfaceAddress ?? null,
      }))
      setSpeedRuns(rows)
    } catch {
      setSpeedProbes([])
      setSpeedRuns([])
    }
  }, [apiFetch, liveApi])

  const refreshUptimeLive = useCallback(async (opts?: { showSpinner?: boolean; pollDevices?: boolean }) => {
    if (!liveApi) return
    if (opts?.showSpinner) setUptimeRefreshBusy(true)
    let collectErr: string | null = null
    try {
      void checkBackend()
      if (opts?.pollDevices) {
        try {
          await apiFetch<{ ok: boolean; lastError?: string | null }>("/api/uptime/collect-now", { method: "POST" })
        } catch (e) {
          collectErr = e instanceof Error ? e.message : "Не удалось опросить устройства (collect-now)"
        }
      }
      await Promise.all([loadLiveOverview(), reloadSpeedData()])
      if (collectErr) {
        setOpError((prev) => (prev ? `${prev} · ${collectErr}` : collectErr))
      }
    } finally {
      if (opts?.showSpinner) setUptimeRefreshBusy(false)
    }
  }, [liveApi, loadLiveOverview, reloadSpeedData, checkBackend, apiFetch])

  const refreshProbeGroupPings = useCallback(
    async (srvId: string, group: { name: string; target: string; probes: PingProbe[] }) => {
      if (!liveApi) return
      const key = probeGroupActionKey(srvId, group)
      setProbeGroupPingBusy((m) => ({ ...m, [key]: true }))
      setOpError(null)
      try {
        await apiFetch<{ ok: boolean; polled: number }>("/api/uptime/probes/collect-group", {
          method: "POST",
          body: JSON.stringify({ probeIds: group.probes.map((p) => p.id) }),
        })
        await loadLiveOverview()
      } catch (e) {
        setOpError(e instanceof Error ? e.message : "Не удалось выполнить ping группы")
      } finally {
        setProbeGroupPingBusy((m) => {
          const next = { ...m }
          delete next[key]
          return next
        })
      }
    },
    [liveApi, apiFetch, loadLiveOverview],
  )

  useEffect(() => {
    if (!liveApi) return
    queueMicrotask(() => { void refreshUptimeLive() })
  }, [liveApi, refreshUptimeLive])

  useEffect(() => {
    if (!liveApi || isPaused) return
    const id = setInterval(() => { void refreshUptimeLive() }, 5_000)
    return () => clearInterval(id)
  }, [liveApi, isPaused, refreshUptimeLive])

  // add-probe form
  const [newSrcId,   setNewSrcId]   = useState("")
  const [newSrcInterface, setNewSrcInterface] = useState("")
  const [newName,    setNewName]    = useState("")
  const [newTarget,  setNewTarget]  = useState("")
  const [newFilter,  setNewFilter]  = useState("—")
  const [editingProbeId, setEditingProbeId] = useState<string | null>(null)
  const [srcInterfaces, setSrcInterfaces] = useState<Array<{ name: string; running: boolean; disabled: boolean }>>([])
  const [srcInterfacesBusy, setSrcInterfacesBusy] = useState(false)

  /** Источник для ping/speed: весь каталог (в т.ч. выключенные в inventory), иначе Home Router нельзя выбрать */
  const selectableSources = useMemo(() => allServers, [allServers])

  const loadSpeedInterfaces = useCallback(async (serverId: string) => {
    if (!liveApi) return
    if (!serverId || speedIfaces[serverId]) return
    const id = Number.parseInt(serverId, 10)
    if (!Number.isFinite(id)) return
    try {
      const res = await apiFetch<{ interfaces: Array<{ name: string; running: boolean; disabled: boolean; addresses: string[] }> }>(`/api/uptime/speed-test/endpoints/${id}/interfaces`)
      setSpeedIfaces((prev) => ({ ...prev, [serverId]: res.interfaces ?? [] }))
    } catch {
      setSpeedIfaces((prev) => ({ ...prev, [serverId]: [] }))
    }
  }, [apiFetch, liveApi, speedIfaces])

  /** После загрузки списков интерфейсов сбросить выбор, если интерфейс не активен или отсутствует в списке */
  useEffect(() => {
    if (!speedSheetOpen) return
    setSpeedDraft((prev) => {
      const rawSrc = speedIfaces[prev.srcServerId]
      const rawDst = speedIfaces[prev.dstServerId]
      let srcInterface = prev.srcInterface
      let dstInterface = prev.dstInterface
      if (rawSrc !== undefined) {
        const active = filterActiveInterfaces(rawSrc)
        if (srcInterface && !active.some((i) => i.name === srcInterface)) srcInterface = ""
      }
      if (rawDst !== undefined) {
        const active = filterActiveInterfaces(rawDst)
        if (dstInterface && !active.some((i) => i.name === dstInterface)) dstInterface = ""
      }
      if (srcInterface === prev.srcInterface && dstInterface === prev.dstInterface) return prev
      return { ...prev, srcInterface, dstInterface }
    })
  }, [speedSheetOpen, speedIfaces])

  useEffect(() => {
    if (!sheetOpen) return
    if (!newSrcId || !selectableSources.some(s => s.id === newSrcId)) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setNewSrcId(selectableSources[0]?.id ?? "")
    }
  }, [sheetOpen, newSrcId, selectableSources])

  useEffect(() => {
    if (!sheetOpen || !newSrcId) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setSrcInterfaces([])
      setNewSrcInterface("")
      return
    }
    if (!liveApi) {
      setSrcInterfaces([])
      setNewSrcInterface("")
      return
    }
    const serverId = Number.parseInt(newSrcId, 10)
    if (!Number.isFinite(serverId)) {
      setSrcInterfaces([])
      setNewSrcInterface("")
      return
    }
    setSrcInterfacesBusy(true)
    void apiFetch<{ interfaces: Array<{ name: string; running: boolean; disabled: boolean }> }>(`/api/uptime/sources/${serverId}/interfaces`)
      .then((data) => {
        const list = filterActiveInterfaces(data.interfaces ?? [])
        setSrcInterfaces(list)
        setNewSrcInterface((prev) => (prev && list.some((i) => i.name === prev) ? prev : ""))
      })
      .catch(() => {
        setSrcInterfaces([])
        setNewSrcInterface("")
      })
      .finally(() => setSrcInterfacesBusy(false))
  }, [sheetOpen, newSrcId, liveApi, apiFetch])

  // servers that have at least one probe (preserve data-order)
  const probedServerIds = useMemo(
    () => [...new Set(probes.map(p => p.srcServerId))],
    [probes],
  )
  const probedServers = useMemo(() => {
    const inCatalog = allServers.filter((s) => probedServerIds.includes(s.id))
    const orphanIds = probedServerIds.filter((id) => !inCatalog.some((s) => s.id === id))
    return [...inCatalog, ...orphanIds.map(orphanSpeedSourceStub)]
  }, [probedServerIds, allServers])

  // live RTT tick
  useEffect(() => {
    const id = setInterval(() => {
      if (liveApi) return
      if (isPausedRef.current) return
      setProbes(prev => prev.map(p => {
        if (!p.enabled || p.status === "down" || p.rtt === null) return p
        const newRtt    = jitter(p.rtt, 0.12)
        const newSeries = [...p.series.slice(1), newRtt]
        return { ...p, rtt: newRtt, series: newSeries }
      }))
    }, 3000)
    return () => clearInterval(id)
  }, [liveApi])

  // live resource tick
  useEffect(() => {
    const id = setInterval(() => {
      if (liveApi) return
      if (isPausedRef.current) return
      setResources(prev => prev.map(r => {
        const srv = allServers.find(s => s.id === r.serverId)
        if (!srv || srv.status !== "online") return r
        if (r.hasData === false) return r
        const newCpu  = Math.min(99, Math.max(1,  r.cpu    + Math.round((Math.random() - 0.48) * 8)))
        const newRam  = Math.min(r.ramTotal - 64, Math.max(256, r.ramUsed + Math.round((Math.random() - 0.5) * 128)))
        const newTemp = r.temp !== undefined
          ? Math.min(90, Math.max(28, r.temp + Math.round((Math.random() - 0.5) * 3)))
          : undefined
        return {
          ...r,
          cpu:           newCpu,
          cpuHistory:    [...r.cpuHistory.slice(1), newCpu],
          ramUsed:       newRam,
          uptimeSeconds: r.uptimeSeconds + 5,
          temp:          newTemp,
        }
      }))
    }, 5000)
    return () => clearInterval(id)
  }, [liveApi, allServers])

  // ── derived ──
  const stats = useMemo(() => ({
    total: probes.length,
    up:    probes.filter(p => p.status === "up").length,
    warn:  probes.filter(p => p.status === "warn").length,
    down:  probes.filter(p => p.status === "down").length,
  }), [probes])

  const alertCount = useMemo(() =>
    resources.filter(r => {
      const s = allServers.find(x => x.id === r.serverId)
      if (!s || s.status !== "online" || r.hasData === false) return false
      const ramPct = Math.round(r.ramUsed / r.ramTotal * 100)
      const hddPct = Math.round(r.hddUsed / r.hddTotal * 100)
      return r.cpu >= 85 || ramPct >= 85 || hddPct >= 85 || (r.temp ?? 0) >= 70
    }).length,
    [resources, allServers],
  )

  const filtered = useMemo(() => probes.filter(p => {
    if (serverFilter.length > 0 && !serverFilter.includes(p.srcServerId)) return false
    if (statusFilter !== "all" && p.status !== statusFilter) return false
    if (search) {
      const q = search.toLowerCase()
      return (
        p.name.toLowerCase().includes(q)   ||
        p.target.toLowerCase().includes(q) ||
        p.filter.toLowerCase().includes(q)
      )
    }
    return true
  }), [probes, serverFilter, statusFilter, search])

  const grouped = useMemo(() => {
    const byServer = new Map<string, Map<string, { name: string; target: string; probes: PingProbe[] }>>()
    for (const p of filtered) {
      if (!byServer.has(p.srcServerId)) byServer.set(p.srcServerId, new Map())
      const key = `${p.name.trim().toLowerCase()}|${p.target.trim().toLowerCase()}`
      const serverMap = byServer.get(p.srcServerId)!
      if (!serverMap.has(key)) serverMap.set(key, { name: p.name, target: p.target, probes: [] })
      serverMap.get(key)!.probes.push(p)
    }
    return probedServers
      .map((server) => ({
        server,
        groups: [...(byServer.get(server.id)?.values() ?? [])]
          .sort((a, b) => (a.target + a.name).localeCompare(b.target + b.name)),
      }))
      .filter((g) => g.groups.length > 0)
  }, [filtered, probedServers])

  const speedGrouped = useMemo(() => {
    const map = new Map<string, SpeedProbeRow[]>()
    for (const p of speedProbes) {
      const arr = map.get(p.srcServerId) ?? []
      arr.push(p)
      map.set(p.srcServerId, arr)
    }
    const ids = [...map.keys()].sort((a, b) => {
      const sa = allServers.find((s) => s.id === a) ?? orphanSpeedSourceStub(a)
      const sb = allServers.find((s) => s.id === b) ?? orphanSpeedSourceStub(b)
      return (sa.host + sa.name).localeCompare(sb.host + sb.name)
    })
    return ids.map((id) => ({
      server: allServers.find((s) => s.id === id) ?? orphanSpeedSourceStub(id),
      probes: (map.get(id) ?? []).sort((a, b) => (a.dstServerId + a.id).localeCompare(b.dstServerId + b.id)),
    }))
  }, [speedProbes, allServers])

  const persistProbes = useCallback((rows: PingProbe[]) => {
    if (!liveApi) return
    void apiFetch("/api/uptime/probes", {
      method: "PUT",
      body: JSON.stringify({
        probes: rows.map((p) => ({
          id: p.id,
          srcServerId: p.srcServerId,
          srcInterface: p.srcInterface || "",
          name: p.name,
          target: p.target,
          filter: p.filter,
          enabled: p.enabled,
          showOnDashboard: p.showOnDashboard === true,
        })),
      }),
    }).catch(() => {})
  }, [apiFetch, liveApi])

  const toggleDashboardStar = useCallback((id: string) => {
    const cur = probes.find((p) => p.id === id)
    if (!cur) return
    const nextVal = !cur.showOnDashboard
    if (liveApi) {
      overviewReqRef.current += 1
    }
    setProbes((prev) => prev.map((p) => (p.id === id ? { ...p, showOnDashboard: nextVal } : p)))
    if (liveApi) {
      void apiFetch(`/api/uptime/probes/${encodeURIComponent(id)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ showOnDashboard: nextVal }),
      })
        .then(() => loadLiveOverview())
        .then(() => {
          if (typeof window !== "undefined") {
            window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
          }
        })
        .catch(() => {
          setProbes((prev) => prev.map((p) => (p.id === id ? { ...p, showOnDashboard: cur.showOnDashboard } : p)))
        })
    } else {
      const s = readMockDashboardStarIds()
      if (nextVal) s.add(id)
      else s.delete(id)
      writeMockDashboardStarIds(s)
      if (typeof window !== "undefined") {
        window.dispatchEvent(new Event(UPTIME_PROBES_CHANGED))
      }
    }
  }, [probes, liveApi, apiFetch, loadLiveOverview])

  const persistSpeedProbes = useCallback((rows: SpeedProbeRow[]) => {
    if (!liveApi) return
    void apiFetch("/api/uptime/speed-probes", {
      method: "PUT",
      body: JSON.stringify({
        probes: rows.map((p) => ({
          id: p.id,
          srcServerId: p.srcServerId,
          dstServerId: p.dstServerId,
          srcInterface: p.srcInterface || "",
          dstInterface: p.dstInterface || "",
          protocol: p.protocol,
          direction: p.direction,
          durationSec: Number.parseInt(p.durationSec, 10) || 10,
          enabled: p.enabled,
          lastRunAt: p.lastRunAt ?? null,
          lastTxAvgMbps: p.lastTxAvgMbps ?? null,
          lastRxAvgMbps: p.lastRxAvgMbps ?? null,
          lastStatus: p.lastStatus ?? null,
          lastError: p.lastError ?? null,
        })),
      }),
    }).catch(() => {})
  }, [apiFetch, liveApi])

  // ── actions ──
  const toggleProbe  = (id: string, v: boolean) =>
    setProbes((prev) => {
      const next = prev.map((x) => x.id === id ? { ...x, enabled: v } : x)
      persistProbes(next)
      return next
    })

  const deleteProbe  = (id: string) => {
    if (!liveApi) {
      const s = readMockDashboardStarIds()
      s.delete(id)
      writeMockDashboardStarIds(s)
    }
    setProbes((prev) => {
      const next = prev.filter((x) => x.id !== id)
      persistProbes(next)
      return next
    })
  }

  const toggleCollapse = (serverId: string) =>
    setCollapsed(prev => {
      const next = new Set(prev)
      if (next.has(serverId)) next.delete(serverId); else next.add(serverId)
      return next
    })

  const collapseAll = useCallback(() =>
    setCollapsed(new Set(grouped.map(g => g.server.id))), [grouped])

  const expandAll = useCallback(() =>
    setCollapsed(new Set()), [])

  const allCollapsed = grouped.length > 0 && grouped.every(g => collapsed.has(g.server.id))

  const toggleServerFilter = (id: string) =>
    setServerFilter(prev =>
      prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
    )

  const cycleStatusFilter = (s: typeof statusFilter) =>
    setStatusFilter(prev => prev === s ? "all" : s)

  const openEditProbe = (probe: PingProbe) => {
    setEditingProbeId(probe.id)
    setNewSrcId(probe.srcServerId)
    setNewSrcInterface(probe.srcInterface || "")
    setNewName(probe.name)
    setNewTarget(probe.target)
    setNewFilter(probe.filter || "—")
    setSheetOpen(true)
  }

  const openAddSheet = () => {
    setEditingProbeId(null)
    setNewSrcId(selectableSources[0]?.id ?? "")
    setNewSrcInterface("")
    setNewName(""); setNewTarget(""); setNewFilter("—")
    setSheetOpen(true)
  }

  const handleSaveProbe = () => {
    if (!newName.trim() || !newTarget.trim() || !newSrcId) return
    const probeBase: PingProbe = {
      id: editingProbeId ?? `p${Date.now()}`,
      srcServerId: newSrcId,
      srcInterface: newSrcInterface,
      name:        newName.trim(),
      target:      newTarget.trim(),
      filter:      newFilter || "—",
      rtt:         null,
      loss:        0,
      status:      "up",
      series:      Array(40).fill(0),
      enabled:     true,
      showOnDashboard: false,
    }
    if (editingProbeId) {
      setProbes((prev) => {
        const next = prev.map((p) => {
        if (p.id !== editingProbeId) return p
        return {
          ...p,
          srcServerId: probeBase.srcServerId,
          srcInterface: probeBase.srcInterface,
          name: probeBase.name,
          target: probeBase.target,
          filter: probeBase.filter,
        }
        })
        persistProbes(next)
        return next
      })
    } else {
      setProbes((prev) => {
        const next = [...prev, probeBase]
        persistProbes(next)
        return next
      })
    }
    setEditingProbeId(null)
    setSheetOpen(false)
  }

  const buildSpeedCommand = useCallback((probe: SpeedProbeRow) => {
    const src = allServers.find((s) => s.id === probe.srcServerId)
    const dst = allServers.find((s) => s.id === probe.dstServerId)
    const rawDst = probe.dstInterface
      ? speedIfaces[probe.dstServerId]?.find((i) => i.name === probe.dstInterface)?.addresses?.[0]
      : undefined
    const dstIp = rawDst ? stripIpCidr(rawDst) : (dst?.host ?? "0.0.0.0")
    const dur = Math.max(3, Number.parseInt(probe.durationSec, 10) || 10)
    return `[${src?.name ?? "src"}] /tool bandwidth-test address=${dstIp} user=<API_назначения> protocol=${probe.protocol} direction=${probe.direction} duration=${dur}s`
  }, [allServers, speedIfaces])

  /** Те же адреса, что при POST /api/uptime/speed-test — для проверки в SSH на узле источника. */
  const speedVerificationRouterOsCli = useMemo(() => {
    const probe = speedDraft
    const src = allServers.find((s) => s.id === probe.srcServerId)
    const dst = allServers.find((s) => s.id === probe.dstServerId)
    if (!src || !dst || probe.srcServerId === probe.dstServerId) return ""

    const dstRaw = probe.dstInterface.trim()
      ? speedIfaces[probe.dstServerId]?.find((i) => i.name === probe.dstInterface)?.addresses?.[0]
      : undefined
    const dstIp = dstRaw ? stripIpCidr(dstRaw) : dst.host.trim()

    const srcRaw = probe.srcInterface.trim()
      ? speedIfaces[probe.srcServerId]?.find((i) => i.name === probe.srcInterface)?.addresses?.[0]
      : undefined
    const srcIp = srcRaw ? stripIpCidr(srcRaw) : src.host.trim()

    const dur = Math.max(3, Number.parseInt(probe.durationSec, 10) || 10)
    const si = probe.srcInterface.trim()
    const di = probe.dstInterface.trim()

    const lines: string[] = []
    lines.push(`# Проверка IP (как backend BTTest: только активные записи /ip/address — без disabled/invalid; статический адрес предпочтительнее dynamic)`)
    lines.push(`# Узел источника: ${src.name}`)
    lines.push(`#   ожидаемый локальный адрес: ${srcIp || "?"}${si ? ` · интерфейс "${si}"` : " · interface не задан — как host/API каталога"}`)
    lines.push(`# Узел назначения: ${dst.name}`)
    lines.push(`#   address для ping и bandwidth-test: ${dstIp || "?"}${di ? ` · интерфейс "${di}" на назначении` : " · host каталога (DNS/API)"}`)
    lines.push("")

    if (si) {
      lines.push(`/ip address print where name="${si.replace(/"/g, '\\"')}"`)
      lines.push("")
    }
    if (di) {
      lines.push(`# Выполнить на узле назначения (${dst.name}) — адрес приёмника BT-сервера:`)
      lines.push(`/ip address print where name="${di.replace(/"/g, '\\"')}"`)
      lines.push("")
    }

    const pingLine =
      `/ping address=${dstIp || "?"} count=4` +
      (si ? ` interface="${si.replace(/"/g, '\\"')}"` : "")
    lines.push(pingLine)
    lines.push("")
    lines.push(
      `/tool bandwidth-test address=${dstIp || "?"} user=<логин_API_назначения> password=<пароль_API_назначения> protocol=${probe.protocol} direction=${probe.direction} duration=${dur}s`,
    )
    lines.push("")
    lines.push(
      `# Подставьте логин/пароль REST API узла «${dst.name}» из раздела «Серверы» (тот же user/password, что для MikrotikClient).`,
    )

    return lines.join("\n")
  }, [speedDraft, allServers, speedIfaces])

  const runSpeedTest = async (probe: SpeedProbeRow) => {
    if (!probe.srcServerId || !probe.dstServerId || probe.srcServerId === probe.dstServerId) return
    setSpeedError(null)
    setSpeedBusy(true)
    const durationSec = Math.max(3, Number.parseInt(probe.durationSec, 10) || 10)
    const runId = `speed-${Date.now()}`
    setSpeedRuns((prev) => [{
      id: runId,
      startedAt: Date.now(),
      srcServerId: probe.srcServerId,
      dstServerId: probe.dstServerId,
      srcInterface: probe.srcInterface || undefined,
      dstInterface: probe.dstInterface || undefined,
      protocol: probe.protocol,
      direction: probe.direction,
      durationSec,
      txAvgMbps: 0,
      rxAvgMbps: 0,
      status: "running" as const,
      command: buildSpeedCommand(probe),
      lines: ["status: running..."],
    }, ...prev].slice(0, 20))
    try {
      if (liveApi) {
        const payload = {
          runId,
          probeId: probe.id,
          srcServerId: Number.parseInt(probe.srcServerId, 10),
          dstServerId: Number.parseInt(probe.dstServerId, 10),
          srcInterface: probe.srcInterface || undefined,
          dstInterface: probe.dstInterface || undefined,
          protocol: probe.protocol,
          direction: probe.direction,
          durationSec,
        }
        const res = await apiFetch<{
          result: {
            txAvgMbps: number
            rxAvgMbps: number
            raw?: Array<Record<string, string>>
            afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null }
            srcAddress?: string | null
            dstAddress?: string | null
            srcInterfaceAddress?: string | null
            dstInterfaceAddress?: string | null
          }
        }>("/api/uptime/speed-test", {
          method: "POST",
          body: JSON.stringify(payload),
        })
        const ap = res.result.afterBtPing
        const lines = (res.result.raw ?? []).flatMap((row) =>
          Object.entries(row).map(([k, v]) => `${k}: ${String(v)}`),
        )
        const pingNote = ap
          ? (ap.error
            ? `after-bt-ping: error: ${ap.error}`
            : `after-bt-ping: rtt=${ap.rttMs ?? "—"}ms loss=${ap.lossPct ?? "—"}%`)
          : ""
        setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({
          ...r,
          txAvgMbps: Math.round(res.result.txAvgMbps),
          rxAvgMbps: Math.round(res.result.rxAvgMbps),
          status: "done",
          afterBtPing: ap ?? null,
          srcAddress: res.result.srcAddress ?? null,
          dstAddress: res.result.dstAddress ?? null,
          srcInterfaceAddress: res.result.srcInterfaceAddress ?? null,
          dstInterfaceAddress: res.result.dstInterfaceAddress ?? null,
          lines: [
            ...(lines.length ? lines : [`tx-total-average: ${Math.round(res.result.txAvgMbps)}Mbps`, `rx-total-average: ${Math.round(res.result.rxAvgMbps)}Mbps`]),
            ...(pingNote ? [pingNote] : []),
          ],
        }) : r))
        const ts = new Date().toISOString()
        setSpeedProbes((prev) => prev.map((p) => p.id === probe.id ? ({
          ...p,
          lastRunAt: ts,
          lastTxAvgMbps: Math.round(res.result.txAvgMbps),
          lastRxAvgMbps: Math.round(res.result.rxAvgMbps),
          lastStatus: "done",
          lastError: "",
          lastPingRttMs: ap?.rttMs ?? null,
          lastPingLossPct: ap?.lossPct ?? null,
          lastPingAt: ts,
          lastPingError: ap?.error ?? "",
        }) : p))
      } else {
        const tx = Math.max(10, Math.round(250 + Math.random() * 500))
        const rx = Math.max(10, Math.round(tx * (0.85 + Math.random() * 0.2)))
        const mockPing = { rttMs: Math.round(8 + Math.random() * 35), lossPct: Math.random() < 0.15 ? Math.round(Math.random() * 25) : 0, error: null as string | null }
        setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({
          ...r,
          txAvgMbps: tx,
          rxAvgMbps: rx,
          status: "done",
          afterBtPing: mockPing,
          srcAddress: allServers.find((s) => s.id === probe.srcServerId)?.host ?? null,
          dstAddress: allServers.find((s) => s.id === probe.dstServerId)?.host ?? null,
          srcInterfaceAddress: null,
          dstInterfaceAddress: null,
          lines: [
            "status: done testing",
            `tx-total-average: ${tx}Mbps`,
            `rx-total-average: ${rx}Mbps`,
            `after-bt-ping: rtt=${mockPing.rttMs}ms loss=${mockPing.lossPct}%`,
          ],
        }) : r))
        const ts = new Date().toISOString()
        setSpeedProbes((prev) => prev.map((p) => p.id === probe.id ? ({
          ...p,
          lastRunAt: ts,
          lastTxAvgMbps: tx,
          lastRxAvgMbps: rx,
          lastStatus: "done",
          lastError: "",
          lastPingRttMs: mockPing.rttMs,
          lastPingLossPct: mockPing.lossPct,
          lastPingAt: ts,
          lastPingError: "",
        }) : p))
      }
    } catch (e) {
      const message = e instanceof Error ? e.message : "Не удалось выполнить speed test"
      setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({
        ...r,
        status: "error",
        lines: [`status: error`, message],
      }) : r))
      setSpeedError(message)
      setSpeedProbes((prev) => prev.map((p) => p.id === probe.id ? ({
        ...p,
        lastRunAt: new Date().toISOString(),
        lastStatus: "error",
        lastError: message,
      }) : p))
    } finally {
      setSpeedBusy(false)
    }
  }

  const updateSpeedProbe = (id: string, patch: Partial<SpeedProbeRow>) => {
    setSpeedProbes((prev) => {
      const nextRows = prev.map((p) => {
        if (p.id !== id) return p
        const next = { ...p, ...patch }
        if (patch.srcServerId && patch.srcServerId === next.dstServerId) {
          next.dstServerId = selectableSources.find((s) => s.id !== patch.srcServerId)?.id ?? patch.srcServerId
        }
        return next
      })
      persistSpeedProbes(nextRows)
      return nextRows
    })
  }

  const openAddSpeedSheet = () => {
    setEditingSpeedProbeId(null)
    const src = selectableSources[0]?.id ?? ""
    const dst = selectableSources.find((s) => s.id !== src)?.id ?? src
    setSpeedDraft({
      id: "",
      srcServerId: src,
      dstServerId: dst,
      srcInterface: "",
      dstInterface: "",
      protocol: "tcp",
      direction: "both",
      durationSec: "10",
      enabled: true,
    })
    setSpeedSheetOpen(true)
    void loadSpeedInterfaces(src)
    void loadSpeedInterfaces(dst)
  }

  const openEditSpeedSheet = (probe: SpeedProbeRow) => {
    setEditingSpeedProbeId(probe.id)
    setSpeedDraft({
      ...probe,
      durationSec: String(probe.durationSec ?? "10"),
    })
    setSpeedSheetOpen(true)
    void loadSpeedInterfaces(probe.srcServerId)
    void loadSpeedInterfaces(probe.dstServerId)
  }

  const saveSpeedProbe = () => {
    if (!speedDraft.srcServerId || !speedDraft.dstServerId) return
    if (editingSpeedProbeId) {
      setSpeedProbes((prev) => {
        const next = prev.map((p) => p.id === editingSpeedProbeId
          ? {
              ...p,
              ...speedDraft,
              id: editingSpeedProbeId,
              durationSec: String(speedDraft.durationSec ?? "10"),
            }
          : p)
        persistSpeedProbes(next)
        return next
      })
    } else {
      setSpeedProbes((prev) => {
        const next = [{
          ...speedDraft,
          id: `sp-${Date.now()}`,
        }, ...prev]
        persistSpeedProbes(next)
        return next
      })
    }
    setEditingSpeedProbeId(null)
    setSpeedSheetOpen(false)
  }

  const deleteSpeedProbe = (id: string) => {
    setSpeedProbes((prev) => {
      const next = prev.filter((p) => p.id !== id)
      persistSpeedProbes(next)
      return next
    })
  }

  const toggleSpeedCollapse = (serverId: string) => {
    setSpeedCollapsed((prev) => {
      const next = new Set(prev)
      if (next.has(serverId)) next.delete(serverId); else next.add(serverId)
      return next
    })
  }

  // ── render ──
  return (
    <div className="flex flex-col h-full">
      <PageHeader
        crumbs={[{ label: "Обзор" }, { label: "Мониторинг" }]}
        actions={
          <>
            {/* Live / Pause toggle */}
            <Button
              variant="outline" size="sm"
              onClick={() => setIsPaused(v => !v)}
              className={cn(isPaused && "border-[var(--status-degraded)]/50 text-[var(--status-degraded-fg)]")}
            >
              {isPaused
                ? <><PlayIcon  className="size-4" />Возобновить</>
                : <><PauseIcon className="size-4" />Пауза</>}
            </Button>

            <Button variant="outline" size="sm" disabled={liveApi && uptimeRefreshBusy} onClick={() => {
              if (liveApi) {
                void refreshUptimeLive({ showSpinner: true, pollDevices: tab === "resources" || tab === "probes" })
                return
              }
              setProbes(mockProbesWithSavedStars(INIT_PROBES))
              setResources(INIT_RESOURCES)
            }}>
              <RefreshCwIcon className={cn("size-4", uptimeRefreshBusy && "animate-spin")} />{liveApi ? "Обновить" : "Сбросить"}
            </Button>

            {tab === "probes" && (
              <Button size="sm" onClick={openAddSheet}>
                <PlusIcon className="size-4" />Новая проба
              </Button>
            )}
            {tab === "speed" && (
              <Button size="sm" onClick={openAddSpeedSheet}>
                <PlusIcon className="size-4" />Новая speed-проба
              </Button>
            )}
          </>
        }
      />

      {/* ── Live indicator ── */}
      {!isPaused && (
        <div className="flex items-center gap-2 px-6 py-1.5 border-b shrink-0 text-[11px]"
          style={{ background: "var(--status-online-bg)", color: "var(--status-online-fg)" }}>
          <span className="size-1.5 rounded-full bg-[var(--status-online)] animate-pulse" />
          {liveApi
            ? (backendStatus === false
              ? "Live: /health не ответил — проверьте URL в настройках; запросы к API выполняются"
              : "Live (backend)")
            : "Демо-режим · пробы/ресурсы локальные; «Обновить» сбрасывает макет"}
        </div>
      )}
      {isPaused && (
        <div className="flex items-center gap-2 px-6 py-1.5 border-b shrink-0 text-[11px]"
          style={{ background: "var(--status-degraded-bg)", color: "var(--status-degraded-fg)" }}>
          <PauseIcon className="size-3" />
          Обновление приостановлено
        </div>
      )}

      {/* ── tab switcher ── */}
      <div className="border-b bg-muted/10 px-6 flex items-center gap-1 shrink-0">
        {(["probes", "resources", "speed"] as const).map(t => (
          <button
            key={t}
            onClick={() => setTab(t)}
            className={cn(
              "px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px",
              tab === t
                ? "border-foreground text-foreground"
                : "border-transparent text-muted-foreground hover:text-foreground",
            )}
          >
            {t === "probes" ? (
              <span className="flex items-center gap-2">
                <span>Пинг-пробы</span>
                {stats.down > 0 && (
                  <span className="inline-flex items-center justify-center min-w-[18px] h-4.5 px-1 rounded-full
                    text-[10px] font-bold bg-red-500 text-white leading-none">
                    {stats.down}
                  </span>
                )}
                <span className="text-[10px] font-mono opacity-50">{probes.length}</span>
              </span>
            ) : t === "resources" ? (
              <span className="flex items-center gap-2">
                <CpuIcon className="size-3.5" />
                <span>Ресурсы</span>
                {alertCount > 0 && (
                  <span className="inline-flex items-center justify-center min-w-[18px] h-4.5 px-1 rounded-full
                    text-[10px] font-bold bg-red-500 text-white leading-none">
                    {alertCount}
                  </span>
                )}
                <span className="text-[10px] font-mono opacity-50">{resources.length}</span>
              </span>
            ) : (
              <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>
            )}
          </button>
        ))}
      </div>

      <div className="flex-1 overflow-y-auto">
        {/* ── resources tab ── */}
        {tab === "resources" && <ResourcesTab resources={resources} serversList={allServers} liveApi={liveApi} />}

        {/* ── speed tab ── */}
        {tab === "speed" && (
          <div className="flex flex-col gap-0">

            {/* ── KPI strip ── */}
            {(() => {
              const doneRuns  = speedRuns.filter(r => r.status === "done")
              const runningCnt = speedRuns.filter(r => r.status === "running").length
              const maxTx     = doneRuns.length ? Math.max(...doneRuns.map(r => r.txAvgMbps)) : null
              const maxRx     = doneRuns.length ? Math.max(...doneRuns.map(r => r.rxAvgMbps)) : null
              return (
                <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 px-6 py-4 border-b bg-muted/10 shrink-0">
                  {[
                    { label: "Speed-пробы",    value: speedProbes.length,           unit: "шт",   color: "" },
                    { label: "Тестов выполнено", value: doneRuns.length,             unit: "run",  color: "" },
                    { label: "Макс TX",         value: maxTx != null ? `${maxTx}` : "—", unit: maxTx != null ? "Мбит/с" : "", color: "text-[var(--chart-tx)]" },
                    { label: "Макс RX",         value: maxRx != null ? `${maxRx}` : "—", unit: maxRx != null ? "Мбит/с" : "", color: "text-[var(--chart-rx)]" },
                  ].map(k => (
                    <Card key={k.label}>
                      <CardContent className="pt-4 pb-3 px-4">
                        <p className="text-xs text-muted-foreground">{k.label}</p>
                        <div className="flex items-baseline gap-1 mt-0.5">
                          <span className={cn("text-2xl font-semibold tabular-nums", k.color)}>{k.value}</span>
                          {k.unit && <span className="text-xs text-muted-foreground">{k.unit}</span>}
                        </div>
                        {runningCnt > 0 && k.label === "Тестов выполнено" && (
                          <p className="text-[11px] text-[var(--status-degraded-fg)] flex items-center gap-1 mt-0.5">
                            <RefreshCwIcon className="size-2.5 animate-spin" />{runningCnt} выполняется
                          </p>
                        )}
                      </CardContent>
                    </Card>
                  ))}
                </div>
              )
            })()}

            {/* ── error ── */}
            {speedError && (
              <div className="mx-6 mt-4 text-xs text-destructive bg-destructive/10 border border-destructive/20 rounded-md px-3 py-2">
                {speedError}
              </div>
            )}

            <div className="flex-1 overflow-y-auto p-6 flex flex-col gap-4">

              {/* ── empty state ── */}
              {speedGrouped.length === 0 && (
                <div className="flex flex-col items-center justify-center py-16 gap-3 text-center">
                  <div className="size-12 rounded-full bg-muted flex items-center justify-center">
                    <ArrowUpDownIcon className="size-5 text-muted-foreground" />
                  </div>
                  <p className="text-sm font-medium">Нет speed-проб</p>
                  <p className="text-xs text-muted-foreground max-w-xs">
                    Создайте пробу через кнопку «Новая speed-проба» в шапке страницы
                  </p>
                  <Button size="sm" variant="outline" className="mt-1" onClick={openAddSpeedSheet}>
                    <PlusIcon className="size-4" />Новая speed-проба
                  </Button>
                </div>
              )}

              {/* ── probe groups ── */}
              {speedGrouped.map(({ server, probes: srvProbes }) => {
                const isCollapsed = speedCollapsed.has(server.id)
                return (
                  <Card key={server.id} className="overflow-hidden py-0 gap-0">
                    {/* server header */}
                    <button
                      onClick={() => toggleSpeedCollapse(server.id)}
                      className="w-full flex items-center gap-2.5 px-4 py-3 hover:bg-muted/30 transition-colors text-left border-b"
                    >
                      {isCollapsed
                        ? <ChevronRightIcon className="size-4 text-muted-foreground shrink-0" />
                        : <ChevronDownIcon  className="size-4 text-muted-foreground shrink-0" />}
                      <StatusDot status={server.status} pulse={server.status === "online"} />
                      <Flag code={server.country} size={16} />
                      <span className="font-mono text-sm font-semibold">{server.name}</span>
                      <TypeChip type={server.type} />
                      <span className="text-xs text-muted-foreground ml-auto">{srvProbes.length} проб</span>
                    </button>

                    {!isCollapsed && (
                      <div className="divide-y divide-border/60">
                        {srvProbes.map((probe) => {
                          const dst = allServers.find((s) => s.id === probe.dstServerId)
                          const run = speedRuns.find((r) =>
                            r.srcServerId === probe.srcServerId &&
                            r.dstServerId === probe.dstServerId &&
                            (r.srcInterface ?? "") === probe.srcInterface &&
                            (r.dstInterface ?? "") === probe.dstInterface,
                          )
                          const viewStatus = run?.status ?? (probe.lastStatus === "error" ? "error" : probe.lastRunAt ? "done" : null)
                          const viewTx = run?.txAvgMbps ?? Math.round(probe.lastTxAvgMbps ?? 0)
                          const viewRx = run?.rxAvgMbps ?? Math.round(probe.lastRxAvgMbps ?? 0)
                          const maxVal = Math.max(viewTx, viewRx, 1)
                          const canRun = probe.enabled && !!probe.srcServerId && !!probe.dstServerId && probe.srcServerId !== probe.dstServerId
                          const viewAfterBtPing =
                            run?.status === "done" && run.afterBtPing
                              ? run.afterBtPing
                              : probe.lastPingAt
                                ? {
                                    rttMs: probe.lastPingRttMs ?? null,
                                    lossPct: probe.lastPingLossPct ?? null,
                                    error: probe.lastPingError?.trim() ? probe.lastPingError : null,
                                  }
                                : null

                          return (
                            <div key={probe.id}
                              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 */}
                                <Toggle 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">
                                  <span className="font-mono text-sm font-medium truncate">{server.name}</span>
                                  <ArrowRightIcon className="size-3.5 text-muted-foreground shrink-0" />
                                  <Flag code={dst?.country ?? "UN"} size={14} className="shrink-0" />
                                  <span className="font-mono text-sm font-medium truncate">{dst?.name ?? probe.dstServerId}</span>
                                </div>

                                {/* param chips */}
                                <div className="flex items-center gap-1 shrink-0 flex-wrap">
                                  <span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold bg-muted/60 text-foreground border-border/60">
                                    {probe.protocol.toUpperCase()}
                                  </span>
                                  <span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium bg-muted/60 text-muted-foreground border-border/60">
                                    {probe.direction === "both" ? "↕" : probe.direction === "transmit" ? "↑" : "↓"}&nbsp;{probe.direction}
                                  </span>
                                  <span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium bg-muted/60 text-muted-foreground border-border/60">
                                    {probe.durationSec}s
                                  </span>
                                  {(probe.srcInterface || probe.dstInterface) && (
                                    <span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-mono bg-muted/60 text-muted-foreground border-border/60">
                                      {probe.srcInterface || "auto"}&nbsp;→&nbsp;{probe.dstInterface || "auto"}
                                    </span>
                                  )}
                                </div>

                                {/* actions */}
                                <div className="flex items-center gap-1 shrink-0">
                                  <Button size="sm" variant="outline" className="h-7 gap-1.5 text-xs"
                                    disabled={speedBusy || !canRun}
                                    onClick={() => void runSpeedTest(probe)}>
                                    <RefreshCwIcon className={cn("size-3", speedBusy && run?.status === "running" && "animate-spin")} />
                                    Запустить
                                  </Button>
                                  <Button size="sm" variant="ghost" className="h-7 text-muted-foreground hover:text-foreground"
                                    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"
                                    onClick={() => deleteSpeedProbe(probe.id)}>
                                    <TrashIcon className="size-3.5" />
                                  </Button>
                                </div>
                              </div>

                              {/* result row */}
                              {viewStatus && (
                                <div className="mt-2.5 ml-9">
                                  {viewStatus === "running" ? (
                                    <p className="text-[11px] text-[var(--status-degraded-fg)] flex items-center gap-1.5">
                                      <RefreshCwIcon className="size-3 animate-spin" />Тест выполняется…
                                    </p>
                                  ) : viewStatus === "error" ? (
                                    <p className="text-[11px] text-[var(--status-offline-fg)]">
                                      {probe.lastError || "Ошибка выполнения теста"}
                                    </p>
                                  ) : (
                                    <div className="flex flex-col gap-1.5 max-w-md">
                                      <div className="flex flex-col gap-1 max-w-md">
                                        {[
                                          { label: "TX", val: viewTx, color: "bg-[var(--chart-tx)]", textColor: "text-[var(--chart-tx)]" },
                                          { label: "RX", val: viewRx, color: "bg-[var(--chart-rx)]", textColor: "text-[var(--chart-rx)]" },
                                        ].map(r => (
                                          <div key={r.label} className="flex items-center gap-2 text-[11px]">
                                            <span className="w-8 font-semibold text-muted-foreground">{r.label}</span>
                                            <div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
                                              <div className={cn("h-full rounded-full", r.color)}
                                                style={{ width: `${Math.max((r.val / maxVal) * 100, r.val > 0 ? 3 : 0)}%` }} />
                                            </div>
                                            <span className={cn("w-24 text-right font-mono tabular-nums font-semibold", r.textColor)}>
                                              {r.val} Мбит/с
                                            </span>
                                          </div>
                                        ))}
                                      </div>
                                      {viewAfterBtPing && (
                                        viewAfterBtPing.error ? (
                                          <p className="text-[11px] text-[var(--status-offline-fg)]">
                                            Ping после BT: {viewAfterBtPing.error}
                                          </p>
                                        ) : (
                                          <div className="flex items-center gap-2 text-[11px]">
                                            <span className="w-8 font-semibold text-violet-600 dark:text-violet-400">Ping</span>
                                            <div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
                                              <div
                                                className={cn(
                                                  "h-full rounded-full",
                                                  viewAfterBtPing.rttMs == null
                                                    ? "bg-amber-500"
                                                    : "bg-violet-500",
                                                )}
                                                style={{
                                                  width: viewAfterBtPing.rttMs == null
                                                    ? "100%"
                                                    : `${Math.max(Math.min((viewAfterBtPing.rttMs / 120) * 100, 100), 4)}%`,
                                                }}
                                              />
                                            </div>
                                            <span className={cn(
                                              "w-24 text-right font-mono tabular-nums font-semibold",
                                              viewAfterBtPing.rttMs == null
                                                ? "text-amber-600 dark:text-amber-400"
                                                : "text-violet-600 dark:text-violet-400",
                                            )}>
                                              {viewAfterBtPing.rttMs == null ? "timeout" : `${viewAfterBtPing.rttMs} мс`}
                                            </span>
                                          </div>
                                        )
                                      )}
                                    </div>
                                  )}
                                </div>
                              )}
                            </div>
                          )
                        })}
                      </div>
                    )}
                  </Card>
                )
              })}

              {/* ── history ── */}
              {speedRuns.length > 0 && (
                <Card className="overflow-hidden">
                  <div className="flex items-center justify-between px-4 py-3 border-b">
                    <p className="text-sm font-medium">История тестов</p>
                    <span className="text-xs text-muted-foreground">{speedRuns.length} запусков</span>
                  </div>
                  <div className="overflow-x-auto">
                    <table className="w-full text-xs">
                      <thead>
                        <tr className="border-b bg-muted/40 text-muted-foreground">
                          {["Время", "Маршрут", "Параметры", "Статус", "TX avg", "RX avg", "Ping после BT"].map(h => (
                            <th key={h} className="px-4 py-2.5 text-left font-medium whitespace-nowrap">{h}</th>
                          ))}
                        </tr>
                      </thead>
                      <tbody className="divide-y divide-border/60">
                        {speedRuns.map((run) => {
                          const src = allServers.find((s) => s.id === run.srcServerId)
                          const dst = allServers.find((s) => s.id === run.dstServerId)
                          const maxVal = Math.max(run.txAvgMbps, run.rxAvgMbps, 1)
                          return (
                            <tr key={run.id} className="hover:bg-muted/20 transition-colors">
                              <td className="px-4 py-2.5 text-muted-foreground whitespace-nowrap tabular-nums font-mono">
                                {new Date(run.startedAt).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit", day: "2-digit", month: "2-digit" })}
                              </td>
                              <td className="px-4 py-2.5 font-mono whitespace-nowrap">
                                <div className="flex items-center gap-1.5">
                                  <Flag code={src?.country ?? "UN"} size={13} />
                                  <span>{src?.name ?? run.srcServerId}</span>
                                  <ArrowRightIcon className="size-3 text-muted-foreground" />
                                  <Flag code={dst?.country ?? "UN"} size={13} />
                                  <span>{dst?.name ?? run.dstServerId}</span>
                                </div>
                                <div className="text-[10px] text-muted-foreground mt-0.5 font-mono">
                                  {run.srcInterfaceAddress && run.dstInterfaceAddress
                                    ? `${run.srcInterfaceAddress} → ${run.dstInterfaceAddress}`
                                    : "внутренние IP: auto/не указаны"}
                                </div>
                              </td>
                              <td className="px-4 py-2.5 text-muted-foreground whitespace-nowrap">
                                <div className="flex items-center gap-1">
                                  <span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold bg-muted/60 border-border/60">
                                    {run.protocol.toUpperCase()}
                                  </span>
                                  <span className="text-muted-foreground/60">·</span>
                                  <span>{run.direction}</span>
                                  <span className="text-muted-foreground/60">·</span>
                                  <span>{run.durationSec}s</span>
                                </div>
                              </td>
                              <td className="px-4 py-2.5">
                                {run.status === "running" ? (
                                  <span className="inline-flex items-center gap-1 text-[var(--status-degraded-fg)]">
                                    <RefreshCwIcon className="size-3 animate-spin" />running
                                  </span>
                                ) : run.status === "error" ? (
                                  <span className="text-[var(--status-offline-fg)]">error</span>
                                ) : (
                                  <span className="text-[var(--status-online-fg)]">done</span>
                                )}
                              </td>
                              <td className="px-4 py-2.5">
                                <div className="flex items-center gap-2 min-w-[120px]">
                                  <div className="w-16 h-1.5 rounded-full bg-muted overflow-hidden">
                                    <div className="h-full rounded-full bg-[var(--chart-tx)]"
                                      style={{ width: `${(run.txAvgMbps / maxVal) * 100}%` }} />
                                  </div>
                                  <span className="font-mono tabular-nums text-[var(--chart-tx)] font-medium whitespace-nowrap">
                                    {run.txAvgMbps} Мбит/с
                                  </span>
                                </div>
                              </td>
                              <td className="px-4 py-2.5">
                                <div className="flex items-center gap-2 min-w-[120px]">
                                  <div className="w-16 h-1.5 rounded-full bg-muted overflow-hidden">
                                    <div className="h-full rounded-full bg-[var(--chart-rx)]"
                                      style={{ width: `${(run.rxAvgMbps / maxVal) * 100}%` }} />
                                  </div>
                                  <span className="font-mono tabular-nums text-[var(--chart-rx)] font-medium whitespace-nowrap">
                                    {run.rxAvgMbps} Мбит/с
                                  </span>
                                </div>
                              </td>
                              <td className="px-4 py-2.5 font-mono tabular-nums whitespace-nowrap">
                                {run.status !== "done" ? "—" : run.afterBtPing?.error ? (
                                  <span className="text-[var(--status-offline-fg)]" title={run.afterBtPing.error}>
                                    ошибка
                                  </span>
                                ) : run.afterBtPing?.rttMs != null ? (
                                  <span className="text-violet-600 dark:text-violet-400">
                                    {run.afterBtPing.rttMs} мс
                                    {run.afterBtPing.lossPct != null && run.afterBtPing.lossPct > 0 && (
                                      <span className="text-amber-600 dark:text-amber-400"> · {run.afterBtPing.lossPct}%</span>
                                    )}
                                  </span>
                                ) : (
                                  <span className="text-amber-600 dark:text-amber-400">
                                    timeout
                                    {run.afterBtPing?.lossPct != null && <span> · {run.afterBtPing.lossPct}%</span>}
                                  </span>
                                )}
                              </td>
                            </tr>
                          )
                        })}
                      </tbody>
                    </table>
                  </div>
                </Card>
              )}

            </div>
          </div>
        )}

        {/* ── probes tab ── */}
        {tab === "probes" && <>

        {opError && (
          <div className="px-6 pt-4">
            <div className="text-xs text-destructive bg-destructive/10 border border-destructive/20 rounded-md px-3 py-2">
              {opError}
            </div>
          </div>
        )}

        {/* ── summary + server filter ── */}
        <div className="border-b bg-muted/20 px-6 py-3 flex items-center gap-3 flex-wrap">
          <StatChip label="Всего"          value={stats.total} />
          <StatChip label="OK"             value={stats.up}   color="emerald" active={statusFilter === "up"}   onClick={() => cycleStatusFilter("up")} />
          <StatChip label="Предупреждение" value={stats.warn} color="amber"   active={statusFilter === "warn"} onClick={() => cycleStatusFilter("warn")} />
          <StatChip label="Недоступен"     value={stats.down} color="red"     active={statusFilter === "down"} onClick={() => cycleStatusFilter("down")} />

          <div className="w-px h-4 bg-border mx-1 shrink-0" />

          {/* server chips */}
          {probedServers.map(s => {
            const active    = serverFilter.includes(s.id)
            const sProbes   = probes.filter(p => p.srcServerId === s.id)
            const hasIssues = sProbes.some(p => p.status !== "up")
            return (
              <button key={s.id} onClick={() => toggleServerFilter(s.id)}
                className={cn(
                  "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium transition-all",
                  active
                    ? "bg-foreground text-background border-foreground"
                    : "border-border text-muted-foreground hover:text-foreground hover:border-foreground/40",
                )}>
                <Flag code={s.country} size={12} />
                <span className="font-mono">{s.name}</span>
                <TypeChip type={s.type} />
                {hasIssues && !active && (
                  <span className="size-1.5 rounded-full bg-amber-500 shrink-0" />
                )}
              </button>
            )
          })}
        </div>

        {/* ── toolbar ── */}
        <div className="px-6 py-3 flex items-center gap-3 border-b flex-wrap">
          <div className="relative min-w-[200px] max-w-xs flex-1">
            <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" />
            <Input className="pl-8 h-8 text-sm" placeholder="Имя, IP-адрес, фильтр…"
              value={search} onChange={e => setSearch(e.target.value)} />
            {search && (
              <button onClick={() => setSearch("")}
                className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
                <XIcon className="size-3.5" />
              </button>
            )}
          </div>

          {(search || statusFilter !== "all" || serverFilter.length > 0) && (
            <Button variant="ghost" size="sm" className="text-muted-foreground h-8 px-2"
              onClick={() => { setSearch(""); setStatusFilter("all"); setServerFilter([]) }}>
              <XIcon className="size-3.5" />Сбросить
            </Button>
          )}

          {/* Collapse / Expand all */}
          <Button variant="outline" size="sm" className="h-8 gap-1.5 ml-auto"
            onClick={allCollapsed ? expandAll : collapseAll}>
            {allCollapsed
              ? <><ChevronDownIcon className="size-3.5" />Развернуть все</>
              : <><ChevronUpIcon   className="size-3.5" />Свернуть все</>}
          </Button>

          <p className="text-xs text-muted-foreground shrink-0">
            {filtered.length} из {probes.length} проб
            <span className="text-muted-foreground/70 hidden md:inline"> · </span>
            <span className="text-muted-foreground/80 hidden md:inline" title="Отображение на дашборде">
              звезда — блок «Активные пробы»
            </span>
          </p>
        </div>

        {/* ── probe groups ── */}
        <div className="p-6 flex flex-col gap-4">

          {grouped.length === 0 && (
            <div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-2">
              <SearchIcon className="size-8 opacity-20" />
              <p className="text-sm">Пробы не найдены</p>
            </div>
          )}

          {grouped.map(({ server: srv, groups: probeGroups }) => {
            const isCollapsed = collapsed.has(srv.id)
            const allServerProbes = probeGroups.flatMap((g) => g.probes)
            const issues      = allServerProbes.filter(p => p.status !== "up").length
            const downCount   = allServerProbes.filter(p => p.status === "down").length
            const warnCount   = allServerProbes.filter(p => p.status === "warn").length

            return (
              <Card key={srv.id} className="overflow-hidden py-0 gap-0">

                {/* server header */}
                <button onClick={() => toggleCollapse(srv.id)}
                  className="w-full flex items-center gap-2.5 px-4 py-3 hover:bg-muted/30 transition-colors text-left border-b">
                  {isCollapsed
                    ? <ChevronRightIcon className="size-4 text-muted-foreground shrink-0" />
                    : <ChevronDownIcon  className="size-4 text-muted-foreground shrink-0" />}
                  <StatusDot status={srv.status} pulse={srv.status === "online"} />
                  <Flag code={srv.country} size={16} />
                  <span className="font-mono text-sm font-semibold">{srv.name}</span>
                  <TypeChip type={srv.type} />
                  <span className="text-xs text-muted-foreground">{srv.site}</span>
                  <div className="ml-auto flex items-center gap-3 text-xs">
                    {downCount > 0 && (
                      <span className="text-red-600 dark:text-red-400 font-medium">{downCount} down</span>
                    )}
                    {warnCount > 0 && (
                      <span className="text-amber-600 dark:text-amber-400 font-medium">{warnCount} warn</span>
                    )}
                    {issues === 0 && (
                      <span className="text-emerald-600 dark:text-emerald-400 font-medium">all ok</span>
                    )}
                    <span className="text-muted-foreground">{allServerProbes.length} проб</span>
                  </div>
                </button>

                {!isCollapsed && (
                  <>
                    <div className="divide-y divide-border/60">
                      {probeGroups.map((group) => {
                        const groupBusyKey = probeGroupActionKey(srv.id, group)
                        const groupBusy = !!probeGroupPingBusy[groupBusyKey]
                        return (
                        <div key={`${group.name}|${group.target}`}>
                          <div className="px-4 py-2 border-b bg-muted/20 flex items-center gap-2 text-xs">
                            <ArrowRightIcon className="size-3.5 text-muted-foreground/60 shrink-0" />
                            <span className="font-medium truncate min-w-0">{group.name}</span>
                            <span className="font-mono text-muted-foreground shrink-0">{group.target}</span>
                            <div className="ml-auto flex items-center gap-2 shrink-0">
                              <span className="text-muted-foreground whitespace-nowrap">{group.probes.length} интерф.</span>
                              <Button
                                type="button"
                                variant="outline"
                                size="sm"
                                className="h-7 gap-1 px-2 text-[11px]"
                                disabled={!liveApi || groupBusy}
                                title={liveApi ? "Ping по всем интерфейсам группы и запись в БД" : "Доступно в режиме Live"}
                                onClick={() => { void refreshProbeGroupPings(srv.id, group) }}
                              >
                                <RefreshCwIcon className={cn("size-3", groupBusy && "animate-spin")} />
                                Обновить
                              </Button>
                            </div>
                          </div>

                          <div className="grid items-center gap-3 px-4 py-1.5 bg-muted/30 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"
                            style={{ gridTemplateColumns: "36px 16px 130px 120px 140px 70px 44px minmax(132px,1fr) 96px 36px 72px" }}>
                            <span />
                            <span />
                            <span>Интерфейс</span>
                            <span>Проба</span>
                            <span>Фильтр</span>
                            <span>RTT</span>
                            <span>Потери</span>
                            <span className="truncate">BT · ping</span>
                            <span>График</span>
                            <span className="text-center" title="На дашборде">★</span>
                            <span>Действия</span>
                          </div>

                          {group.probes.map((p) => {
                            const linkedSp = findLinkedSpeedProbe(p, speedProbes, allServers, speedIfaces)
                            return (
                            <div key={p.id} className="border-b border-border/40 last:border-b-0">
                            <div
                              className={cn(
                                "grid items-center gap-3 px-4 py-2.5 hover:bg-muted/20 transition-colors",
                                !p.enabled && "opacity-40",
                              )}
                              style={{ gridTemplateColumns: "36px 16px 130px 120px 140px 70px 44px minmax(132px,1fr) 96px 36px 72px" }}>
                              <Toggle checked={p.enabled} onChange={v => toggleProbe(p.id, v)} />
                              <StatusDot
                                status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
                                pulse={p.status === "up" && p.enabled}
                              />
                              <span className="font-mono text-xs text-muted-foreground truncate">{p.srcInterface || "auto"}</span>
                              <span className="text-sm font-medium truncate">{p.name}</span>
                              {p.filter === "—"
                                ? <span className="text-muted-foreground/40 text-xs">—</span>
                                : <span className="text-[11px] font-medium text-sky-600 dark:text-sky-400 truncate">{p.filter}</span>
                              }
                              <span className={cn("font-mono text-sm font-semibold tabular-nums", rttColor(p.rtt, p.loss))}>
                                {p.rtt === null ? "—" : `${p.rtt}мс`}
                              </span>
                              <span className={cn("font-mono text-xs tabular-nums",
                                p.loss === 0    ? "text-muted-foreground"              :
                                p.loss >= 100   ? "text-red-600 dark:text-red-400"    :
                                p.loss > 5      ? "text-red-600 dark:text-red-400"    :
                                                  "text-amber-600 dark:text-amber-400",
                              )}>
                                {p.loss}%
                              </span>
                              <div className="min-w-0 flex flex-col gap-0.5 justify-center">
                                {!linkedSp?.lastRunAt ? (
                                  <span className="text-muted-foreground/35 text-[10px]">—</span>
                                ) : linkedSp.lastStatus === "error" && linkedSp.lastError ? (
                                  <span className="text-[10px] text-[var(--status-offline-fg)] truncate" title={linkedSp.lastError}>
                                    BT: ошибка
                                  </span>
                                ) : (
                                  <>
                                    <span className="text-[10px] font-mono tabular-nums text-muted-foreground truncate">
                                      {linkedSp.lastTxAvgMbps != null && linkedSp.lastRxAvgMbps != null
                                        ? `${Math.round(linkedSp.lastTxAvgMbps)}/${Math.round(linkedSp.lastRxAvgMbps)} Мбит/с`
                                        : "—"}
                                    </span>
                                    {linkedSp.lastPingError?.trim() ? (
                                      <span className="text-[10px] text-[var(--status-offline-fg)] truncate" title={linkedSp.lastPingError}>
                                        ping: сбой
                                      </span>
                                    ) : linkedSp.lastPingRttMs != null ? (
                                      <span className="text-[10px] font-mono tabular-nums text-violet-600 dark:text-violet-400 truncate">
                                        ping {linkedSp.lastPingRttMs} мс
                                        {linkedSp.lastPingLossPct != null && linkedSp.lastPingLossPct > 0 && (
                                          <span className="text-amber-600 dark:text-amber-400"> · {linkedSp.lastPingLossPct}%</span>
                                        )}
                                      </span>
                                    ) : linkedSp.lastPingAt ? (
                                      <span className="text-[10px] text-amber-600 dark:text-amber-400 truncate">
                                        ping timeout
                                        {linkedSp.lastPingLossPct != null && <span> · {linkedSp.lastPingLossPct}%</span>}
                                      </span>
                                    ) : null}
                                  </>
                                )}
                              </div>
                              <Sparkline data={p.series} width={96} height={24} color={probeSparkColor(p.status)} filled />
                              <div className="flex justify-center">
                                <Button
                                  type="button"
                                  size="sm"
                                  variant="ghost"
                                  className="size-7 p-0 text-muted-foreground hover:text-amber-500"
                                  onClick={() => toggleDashboardStar(p.id)}
                                  title={p.showOnDashboard ? "Убрать с дашборда" : "Показать на дашборде"}
                                  aria-pressed={p.showOnDashboard === true}
                                >
                                  <StarIcon
                                    className={cn(
                                      "size-3.5",
                                      p.showOnDashboard
                                        ? "fill-amber-400 text-amber-500"
                                        : "text-muted-foreground",
                                    )}
                                  />
                                </Button>
                              </div>
                              <div className="flex justify-end gap-0.5">
                                <Button size="sm" variant="ghost"
                                  className="size-7 p-0 text-muted-foreground hover:text-foreground"
                                  onClick={() => openEditProbe(p)} title="Редактировать пробу">
                                  <PencilIcon className="size-3.5" />
                                </Button>
                                <Button size="sm" variant="ghost"
                                  className="size-7 p-0 text-muted-foreground hover:text-destructive"
                                  onClick={() => deleteProbe(p.id)} title="Удалить пробу">
                                  <TrashIcon className="size-3.5" />
                                </Button>
                              </div>
                            </div>
                            <Collapsible
                              open={probeRttChartOpen[p.id] ?? false}
                              onOpenChange={(open) => setProbeRttChartOpen((m) => ({ ...m, [p.id]: open }))}
                            >
                              <CollapsibleTrigger
                                className={cn(
                                  "flex w-full items-center gap-2 border-t border-border/50 bg-muted/15 px-4 py-1.5 text-left text-xs text-muted-foreground",
                                  "outline-none hover:bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
                                )}
                              >
                                <ChevronDownIcon
                                  className={cn(
                                    "size-3.5 shrink-0 transition-transform duration-200",
                                    probeRttChartOpen[p.id] && "rotate-180",
                                  )}
                                />
                                <span>
                                  Подробный график RTT
                                  <span className="font-mono tabular-nums text-muted-foreground/80 ml-1">
                                    ({p.series.length} точ.)
                                  </span>
                                </span>
                              </CollapsibleTrigger>
                              <CollapsibleContent>
                                <div className="border-t border-border/50 bg-muted/5 px-4 py-3">
                                  <ProbePingRttDetailChart
                                    series={p.series}
                                    status={p.status}
                                    probeName={p.name}
                                    target={group.target}
                                  />
                                </div>
                              </CollapsibleContent>
                            </Collapsible>
                            </div>
                            )
                          })}

                          <button
                            onClick={() => {
                              setEditingProbeId(null)
                              setNewSrcId(srv.id)
                              setNewSrcInterface("")
                              setNewName(group.name); setNewTarget(group.target); setNewFilter("—")
                              setSheetOpen(true)
                            }}
                            className="w-full flex items-center gap-2 px-4 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
                            <PlusIcon className="size-3.5" />
                            Добавить интерфейс для {group.name} ({group.target})
                          </button>
                        </div>
                        )
                      })}
                    </div>
                  </>
                )}
              </Card>
            )
          })}
        </div>
        </>}
      </div>

      {/* ── add speed-probe sheet ── */}
      <Sheet open={speedSheetOpen} onOpenChange={(v) => {
        if (!v) {
          setSpeedSheetOpen(false)
          setEditingSpeedProbeId(null)
          return
        }
        setSpeedSheetOpen(true)
      }}>
        <SheetContent side="right" className="sm:max-w-[420px] flex flex-col p-0" showCloseButton={false}>
          <SheetHeader className="px-5 pt-5 pb-4 border-b shrink-0">
            <div className="flex items-center justify-between gap-2">
              <div>
                <SheetTitle className="text-base">{editingSpeedProbeId ? "Редактирование speed-пробы" : "Новая speed-проба"}</SheetTitle>
                <SheetDescription className="text-xs mt-0.5">
                  {editingSpeedProbeId ? "Измените параметры BT-пробы" : "BTTest между выбранными серверами"}
                </SheetDescription>
              </div>
              <Button variant="ghost" size="icon-sm" onClick={() => setSpeedSheetOpen(false)}>
                <XIcon className="size-4" />
              </Button>
            </div>
          </SheetHeader>

          <div className="flex-1 overflow-y-auto px-5 py-5 flex flex-col gap-5">
            <Field label="Источник">
              <ServerPickerCards
                options={selectableSources}
                selectedId={speedDraft.srcServerId}
                blockedId={speedDraft.dstServerId}
                onSelect={(nextSrc) => {
                  const nextDst = speedDraft.dstServerId === nextSrc
                    ? (selectableSources.find((s) => s.id !== nextSrc)?.id ?? nextSrc)
                    : speedDraft.dstServerId
                  setSpeedDraft((prev) => ({ ...prev, srcServerId: nextSrc, dstServerId: nextDst, srcInterface: "" }))
                  void loadSpeedInterfaces(nextSrc)
                }}
              />
            </Field>

            <Field label="Назначение">
              <ServerPickerCards
                options={selectableSources.filter((s) => s.id !== speedDraft.srcServerId)}
                selectedId={speedDraft.dstServerId}
                onSelect={(nextDst) => {
                  setSpeedDraft((prev) => ({ ...prev, dstServerId: nextDst, dstInterface: "" }))
                  void loadSpeedInterfaces(nextDst)
                }}
              />
            </Field>

            <Field label="Интерфейс источника">
              <InterfacePickerCards
                value={speedDraft.srcInterface}
                onChange={(v) => setSpeedDraft((prev) => ({ ...prev, srcInterface: v }))}
                options={filterActiveInterfaces(speedIfaces[speedDraft.srcServerId] ?? [])}
                autoLabel="auto"
              />
            </Field>

            <Field label="Интерфейс назначения">
              <InterfacePickerCards
                value={speedDraft.dstInterface}
                onChange={(v) => setSpeedDraft((prev) => ({ ...prev, dstInterface: v }))}
                options={filterActiveInterfaces(speedIfaces[speedDraft.dstServerId] ?? [])}
                autoLabel="auto"
              />
            </Field>

            <div className="grid grid-cols-3 gap-3">
              <Field label="Протокол">
                <SegmentedControl
                  value={speedDraft.protocol}
                  onChange={(v) => setSpeedDraft((prev) => ({ ...prev, protocol: v }))}
                  options={[
                    { value: "tcp", label: "TCP" },
                    { value: "udp", label: "UDP" },
                  ]}
                />
              </Field>
              <Field label="Direction">
                <SegmentedControl
                  value={speedDraft.direction}
                  onChange={(v) => setSpeedDraft((prev) => ({ ...prev, direction: v }))}
                  options={[
                    { value: "both", label: "both" },
                    { value: "transmit", label: "tx" },
                    { value: "receive", label: "rx" },
                  ]}
                />
              </Field>
              <Field label="Сек">
                <Input className="h-9" value={speedDraft.durationSec} onChange={(e) => setSpeedDraft((prev) => ({ ...prev, durationSec: e.target.value }))} />
              </Field>
            </div>

            {speedDraft.srcServerId &&
              speedDraft.dstServerId &&
              speedDraft.srcServerId !== speedDraft.dstServerId &&
              speedVerificationRouterOsCli && (
              <div className="rounded-lg border border-border/80 bg-muted/30 p-3 space-y-2">
                <div className="flex items-center justify-between gap-2">
                  <p className="text-xs font-medium text-muted-foreground leading-snug">
                    RouterOS CLI — проверка адресов
                  </p>
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    className="h-8 shrink-0 gap-1.5 text-xs"
                    onClick={() => {
                      void navigator.clipboard.writeText(speedVerificationRouterOsCli).then(() => {
                        /* feedback через title */
                      })
                    }}
                    title="Копировать в буфер"
                  >
                    <CopyIcon className="size-3.5" />
                    Копировать
                  </Button>
                </div>
                <pre className="text-[10px] font-mono leading-relaxed text-foreground/90 whitespace-pre-wrap break-all max-h-[240px] overflow-y-auto rounded-md bg-background/60 border border-border/50 p-2.5">
                  {speedVerificationRouterOsCli}
                </pre>
                <p className="text-[10px] text-muted-foreground leading-snug">
                  Команды выполняйте на узле источника (SSH или Terminal в MikrotikManager). Если IP показываются как «?», перезагрузите список интерфейсов (смените сервер или закройте и снова откройте форму) — подтянутся адреса с RouterOS.
                </p>
              </div>
            )}
          </div>

          <SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
            <Button variant="outline" className="flex-1" onClick={() => { setSpeedSheetOpen(false); setEditingSpeedProbeId(null) }}>Отмена</Button>
            <Button className="flex-1" onClick={saveSpeedProbe} disabled={!speedDraft.srcServerId || !speedDraft.dstServerId || speedDraft.srcServerId === speedDraft.dstServerId}>
              {editingSpeedProbeId ? <PencilIcon className="size-4" /> : <PlusIcon className="size-4" />}
              {editingSpeedProbeId ? "Сохранить" : "Добавить"}
            </Button>
          </SheetFooter>
        </SheetContent>
      </Sheet>

      {/* ── add probe sheet ── */}
      <Sheet open={sheetOpen} onOpenChange={(v) => {
        if (!v) {
          setSheetOpen(false)
          setEditingProbeId(null)
          return
        }
        setSheetOpen(true)
      }}>
        <SheetContent side="right" className="sm:max-w-[420px] flex flex-col p-0" showCloseButton={false}>

          <SheetHeader className="px-5 pt-5 pb-4 border-b shrink-0">
            <div className="flex items-center justify-between gap-2">
              <div>
                <SheetTitle className="text-base">{editingProbeId ? "Редактирование пробы" : "Новая проба"}</SheetTitle>
                <SheetDescription className="text-xs mt-0.5">
                  Ping от выбранного сервера к целевому хосту
                </SheetDescription>
              </div>
              <Button variant="ghost" size="icon-sm" onClick={() => setSheetOpen(false)}>
                <XIcon className="size-4" />
              </Button>
            </div>
          </SheetHeader>

          <div className="flex-1 overflow-y-auto px-5 py-5 flex flex-col gap-5">

            {/* from → to visual */}
            <div className="flex items-center gap-2 rounded-lg bg-muted/40 border px-4 py-3">
              <div className="flex-1 min-w-0">
                <p className="text-[10px] text-muted-foreground uppercase tracking-widest mb-0.5">Источник</p>
                {newSrcId ? (() => {
                  const s = allServers.find(x => x.id === newSrcId)
                  return s ? (
                    <div className="flex items-center gap-1.5">
                      <Flag code={s.country} size={14} />
                      <span className="text-sm font-mono font-medium truncate">{s.name}</span>
                      <TypeChip type={s.type} />
                    </div>
                  ) : null
                })() : <span className="text-xs text-muted-foreground">не выбран</span>}
              </div>
              <ArrowRightIcon className="size-4 text-muted-foreground/40 shrink-0" />
              <div className="flex-1 min-w-0 text-right">
                <p className="text-[10px] text-muted-foreground uppercase tracking-widest mb-0.5">Цель</p>
                <p className="text-sm font-mono truncate">
                  {newTarget || <span className="text-muted-foreground/40">0.0.0.0</span>}
                </p>
              </div>
            </div>

            <Field
              label="Источник (кто пингует)"
              hint="— весь каталог, в т.ч. выключенные в инвентаре (Home Router часто «выкл.», но доступен по LAN для ping)"
            >
              <ServerPickerCards
                options={selectableSources}
                selectedId={newSrcId}
                onSelect={(id) => setNewSrcId(id)}
              />
            </Field>

            <Field label="Интерфейс источника" hint="(необязательно)">
              <InterfacePickerCards
                value={newSrcInterface}
                onChange={setNewSrcInterface}
                options={filterActiveInterfaces(srcInterfaces)}
                autoLabel="авто (по маршруту)"
                busy={srcInterfacesBusy}
              />
            </Field>

            <Field label="Имя пробы">
              <Input className="h-9 text-sm" placeholder="youtube.com"
                value={newName} onChange={e => setNewName(e.target.value)} />
            </Field>

            <Field label="Целевой IP / хост">
              <Input className="h-9 text-sm font-mono" placeholder="142.250.74.110"
                value={newTarget} onChange={e => setNewTarget(e.target.value)} />
            </Field>

            <Field label="Связанный фильтр" hint="(необязательно)">
              <LinkedFilterPickerCards
                value={newFilter}
                onChange={setNewFilter}
                items={filters}
              />
            </Field>
          </div>

          <SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
            <Button variant="outline" className="flex-1" onClick={() => { setSheetOpen(false); setEditingProbeId(null) }}>
              Отмена
            </Button>
            <Button className="flex-1"
              disabled={!newName.trim() || !newTarget.trim() || !newSrcId}
              onClick={handleSaveProbe}>
              {editingProbeId ? <PencilIcon className="size-4" /> : <PlusIcon className="size-4" />}
              {editingProbeId ? "Сохранить" : "Добавить"}
            </Button>
          </SheetFooter>
        </SheetContent>
      </Sheet>
    </div>
  )
}
