"use client"

import { useState, useMemo, useEffect, useRef, useCallback } from "react"
import { PageHeader } from "@/components/page-header"
import { FormField, FormToggle, SegmentedControl } from "@/components/form-kit"
import { Frame, FramePanel } from "@/components/reui/frame"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Flag } from "@/components/flag"
import { StatusDot } from "@/components/status-dot"
import { Sparkline } from "@/components/sparkline"
import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar } from "@/components/data-page-toolbar"
import {
  UptimeResourcesDataGrid,
  type UptimeResourceRow,
} from "@/components/data-grids/uptime-resources-data-grid"
import { UptimeSpeedHistoryDataGrid } from "@/components/data-grids/uptime-speed-history-data-grid"
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 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>
  )
}

/** Активный интерфейс 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 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 [resSearch,  setResSearch]  = useState("")
  const [typeFilter, setTypeFilter] = useState<ResTypeFilter>("all")

  const rows = useMemo((): UptimeResourceRow[] => 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 => serversList.some(s => s.id === r.serverId)), [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)
      )
    }
    return list
  }, [rows, typeFilter, resSearch])

  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 && (
        <Alert variant="destructive">
          <ServerCrashIcon />
          <AlertTitle className="text-xs mb-1">
            {alerts.length} {alerts.length === 1 ? "сервер требует внимания" : "сервера требуют внимания"}
          </AlertTitle>
          <AlertDescription>
            <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-destructive/30 bg-destructive/10 px-2 py-0.5">
                    <Flag code={s.country} size={10} />
                    {s.name} — {issues.join(", ")}
                  </span>
                )
              })}
            </div>
          </AlertDescription>
        </Alert>
      )}

      <KpiStatGrid
        aria-label="Сводка ресурсов"
        items={[
          {
            id: "servers",
            label: "Серверов всего",
            value: rows.length,
            icon: <ServerIcon className="size-4" />,
            iconClassName: "text-muted-foreground",
          },
          {
            id: "cpu",
            label: "Средний CPU",
            value: `${avgCpu}%`,
            icon: <CpuIcon className="size-4" />,
            iconClassName: avgCpu >= 85 ? "text-destructive" : avgCpu >= 70 ? "text-warning" : "text-success",
            variant: avgCpu >= 85 ? "destructive" : avgCpu >= 70 ? "warning" : "default",
          },
          {
            id: "ram",
            label: "Средний RAM",
            value: `${avgRam}%`,
            icon: <HardDriveIcon className="size-4" />,
            iconClassName: avgRam >= 85 ? "text-destructive" : avgRam >= 70 ? "text-warning" : "text-success",
            variant: avgRam >= 85 ? "destructive" : avgRam >= 70 ? "warning" : "default",
          },
          {
            id: "high-cpu",
            label: "CPU > 85%",
            value: highCpu,
            icon: <AlertCircleIcon className="size-4" />,
            iconClassName: highCpu > 0 ? "text-destructive" : "text-muted-foreground",
            variant: highCpu > 0 ? "destructive" : "default",
          },
          {
            id: "high-ram",
            label: "RAM > 85%",
            value: highRam,
            icon: <AlertCircleIcon className="size-4" />,
            iconClassName: highRam > 0 ? "text-destructive" : "text-muted-foreground",
            variant: highRam > 0 ? "destructive" : "default",
          },
          {
            id: "high-hdd",
            label: "Диск > 85%",
            value: highHdd,
            icon: <AlertCircleIcon className="size-4" />,
            iconClassName: highHdd > 0 ? "text-warning" : "text-muted-foreground",
            variant: highHdd > 0 ? "warning" : "default",
          },
        ]}
      />

      {/* ── Table ─────────────────────────────────────────────────────────── */}
      <DataPageCard>
        <DataPageToolbar
          segmented={{
            value: typeFilter,
            onChange: setTypeFilter,
            options: typeOpts,
          }}
          search={resSearch}
          onSearchChange={setResSearch}
          searchPlaceholder="Поиск по имени, площадке…"
          countLabel={`${visible.length} из ${rows.length} серверов`}
          actions={
            <Button variant="outline" size="sm" className="h-8 gap-1.5 shrink-0" onClick={exportCsv}>
              <DownloadIcon className="size-3.5" />
              CSV
            </Button>
          }
        />
        <UptimeResourcesDataGrid rows={visible} />
      </DataPageCard>

      <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 { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
  /** При mode=live всегда ходим на backend. Нельзя требовать backendStatus===true: до ответа /health там undefined — иначе обзор/«Обновить» молчат. */
  const liveApi = mode === "live"
  const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])

  const [allServers, setAllServers] = useState<Server[]>([])
  const [tab,          setTab]          = useState<"probes" | "resources" | "speed">("probes")
  const [probes,       setProbes]       = useState<PingProbe[]>([])
  const [resources,    setResources]    = useState<ServerResource[]>([])
  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) return
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setAllServers(mockServers)
    setProbes(mockProbesWithSavedStars(INIT_PROBES))
    setResources(INIT_RESOURCES)
  }, [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="px-6 py-4 border-b bg-muted/10 shrink-0">
                  <KpiStatGrid
                    aria-label="Сводка speed-проб"
                    items={[
                      {
                        id: "probes",
                        label: "Speed-пробы",
                        value: `${speedProbes.length} шт`,
                        icon: <ArrowUpDownIcon className="size-4" />,
                        iconClassName: "text-muted-foreground",
                      },
                      {
                        id: "runs",
                        label: "Тестов выполнено",
                        value: `${doneRuns.length} run`,
                        hint: runningCnt > 0 ? `${runningCnt} выполняется` : undefined,
                        icon: <PlayIcon className="size-4" />,
                        iconClassName: runningCnt > 0 ? "text-warning" : "text-muted-foreground",
                        variant: runningCnt > 0 ? "warning" : "default",
                      },
                      {
                        id: "max-tx",
                        label: "Макс TX",
                        value: maxTx != null ? `${maxTx} Мбит/с` : "—",
                        icon: <ArrowUpIcon className="size-4" />,
                        iconClassName: "text-info",
                      },
                      {
                        id: "max-rx",
                        label: "Макс RX",
                        value: maxRx != null ? `${maxRx} Мбит/с` : "—",
                        icon: <ArrowDownIcon className="size-4" />,
                        iconClassName: "text-success",
                      },
                    ]}
                  />
                </div>
              )
            })()}

            {/* ── error ── */}
            {speedError && (
              <div className="mx-6 mt-4">
                <Alert variant="destructive" className="py-2 text-xs">
                  <AlertCircleIcon />
                  <AlertDescription>{speedError}</AlertDescription>
                </Alert>
              </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 (
                  <Frame key={server.id} dense className="w-full overflow-hidden">
                    <FramePanel className="p-0 overflow-hidden">
                    {/* 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 */}
                                <FormToggle 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>
                    )}
                  </FramePanel>
                  </Frame>
                )
              })}

              {/* ── history ── */}
              {speedRuns.length > 0 && (
                <DataPageCard>
                  <div className="flex items-center justify-between px-5 py-3 border-b">
                    <p className="text-sm font-medium">История тестов</p>
                    <span className="text-xs text-muted-foreground">{speedRuns.length} запусков</span>
                  </div>
                  <UptimeSpeedHistoryDataGrid runs={speedRuns} servers={allServers} />
                </DataPageCard>
              )}

            </div>
          </div>
        )}

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

        {opError && (
          <div className="px-6 pt-4">
            <Alert variant="destructive" className="py-2 text-xs">
              <AlertCircleIcon />
              <AlertDescription>{opError}</AlertDescription>
            </Alert>
          </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 (
              <Frame key={srv.id} dense className="w-full overflow-hidden">
                <FramePanel className="p-0 overflow-hidden">

                {/* 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" }}>
                              <FormToggle 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>
                  </>
                )}
              </FramePanel>
              </Frame>
            )
          })}
        </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">
            <FormField 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)
                }}
              />
            </FormField>

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

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

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

            <div className="grid grid-cols-3 gap-3">
              <FormField label="Протокол">
                <SegmentedControl
                  value={speedDraft.protocol}
                  onChange={(v) => setSpeedDraft((prev) => ({ ...prev, protocol: v }))}
                  options={[
                    { value: "tcp", label: "TCP" },
                    { value: "udp", label: "UDP" },
                  ]}
                />
              </FormField>
              <FormField 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" },
                  ]}
                />
              </FormField>
              <FormField label="Сек">
                <Input className="h-9" value={speedDraft.durationSec} onChange={(e) => setSpeedDraft((prev) => ({ ...prev, durationSec: e.target.value }))} />
              </FormField>
            </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>

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

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

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

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

            <FormField label="Связанный фильтр" hint="(необязательно)">
              <LinkedFilterPickerCards
                value={newFilter}
                onChange={setNewFilter}
                items={filters}
              />
            </FormField>
          </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>
  )
}
