From 0d0cec1f9f44912b7e3243c3f04003101b4070b5 Mon Sep 17 00:00:00 2001 From: dv Date: Thu, 9 Apr 2026 11:29:20 +0300 Subject: [PATCH] =?UTF-8?q?=D1=8D=D1=8D=D1=8D=D1=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DeviceDetail.tsx | 15 ++++++++ frontend/src/pages/InventoryObjectDetail.tsx | 25 +----------- frontend/src/utils/deviceUrl.ts | 40 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 frontend/src/utils/deviceUrl.ts diff --git a/frontend/src/pages/DeviceDetail.tsx b/frontend/src/pages/DeviceDetail.tsx index 44ae1a6..0ee3c8b 100644 --- a/frontend/src/pages/DeviceDetail.tsx +++ b/frontend/src/pages/DeviceDetail.tsx @@ -4,6 +4,7 @@ import { DeleteOutlined, EditOutlined, EyeOutlined, + LinkOutlined, } from '@ant-design/icons' import { Badge, @@ -29,6 +30,7 @@ import { useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useAlerts, useAcknowledgeAlert, useResolveAlert, type AlertItem } from '../api/alerts' import { useDevice, useDeleteDevice, useObject } from '../api/objects' +import { getDeviceWebUrl } from '../utils/deviceUrl' import { useSnapshots, useSnapshotDiff } from '../api/snapshots' import { useDeviceTasks, useTask, type Task } from '../api/tasks' import { DeviceEditModal } from '../components/DeviceEditModal' @@ -397,6 +399,7 @@ export function DeviceDetailPage() { const effectiveStatus = liveStatus?.status ?? device?.status ?? 'unknown' const effectivePing = liveStatus !== null ? liveStatus.ping_ms : (device?.last_ping_ms ?? null) + const webUrl = device ? getDeviceWebUrl(device) : null const handleDelete = async () => { try { @@ -581,6 +584,18 @@ export function DeviceDetailPage() { + {webUrl && ( + + + + )} diff --git a/frontend/src/pages/InventoryObjectDetail.tsx b/frontend/src/pages/InventoryObjectDetail.tsx index c0deb97..bb982b7 100644 --- a/frontend/src/pages/InventoryObjectDetail.tsx +++ b/frontend/src/pages/InventoryObjectDetail.tsx @@ -56,34 +56,11 @@ import { CategoryTag, DeviceStatusBadge } from '../components/StatusBadge' import { DeviceEditModal } from '../components/DeviceEditModal' import { DiscoveryDrawer } from '../components/DiscoveryDrawer' import { stripEmpty } from '../utils/form' +import { getDeviceWebUrl } from '../utils/deviceUrl' import dayjs from 'dayjs' const SERVER_CATEGORIES = new Set(['main_server', 'vm', 'router']) -/** Возвращает URL веб-интерфейса устройства или null, если ссылка не применима. */ -function getDeviceWebUrl(device: DeviceRow): string | null { - const hostname = (device.hostname ?? '').toLowerCase() - - // Proxmox — определяем по hostname - if (hostname.includes('proxmox') || hostname.includes('pve')) { - return `http://${device.ip}:8006` - } - - // RTSP-прокси (go2rtc/mediamtx) — определяем по capabilities - if (device.capabilities?.includes('rtsp')) { - return `http://${device.ip}:1984` - } - - switch (device.category) { - case 'main_server': return `http://${device.ip}:80` - case 'router': return `http://${device.ip}:80` // MikroTik WebFig - case 'embedded': return `http://${device.ip}:5000` - case 'camera': return `http://${device.ip}:80` - // vm без proxmox/pve в hostname — asterisk, db и прочие, ссылка не нужна - default: return null - } -} - const rackLabel = (name: string, type: string | null | undefined): string => type ? `${name} ${type}` : name const DEVICE_COL_SPAN = 8 diff --git a/frontend/src/utils/deviceUrl.ts b/frontend/src/utils/deviceUrl.ts new file mode 100644 index 0000000..9048757 --- /dev/null +++ b/frontend/src/utils/deviceUrl.ts @@ -0,0 +1,40 @@ +import type { DeviceItem } from '../api/objects' + +/** + * Возвращает URL веб-интерфейса устройства или null, если ссылка не применима. + * + * Приоритет проверок: + * 1. Proxmox — hostname содержит «proxmox» или «pve» → :8006 + * 2. RTSP-прокси (go2rtc/mediamtx) — capabilities содержит «rtsp» + * ИЛИ device_meta.cls === «rtsp_server» + * ИЛИ device_meta.plugin содержит «rtsp» → :1984 + * 3. Категория: main_server/:80, router (WebFig)/:80, embedded/:5000, camera/:80 + * 4. vm без proxmox/pve, io_board, bank_terminal, cash_register, other → null + */ +export function getDeviceWebUrl(device: DeviceItem): string | null { + const hostname = (device.hostname ?? '').toLowerCase() + const metaCls = String(device.device_meta?.cls ?? '').toLowerCase() + const metaPlugin = String(device.device_meta?.plugin ?? '').toLowerCase() + + // Proxmox + if (hostname.includes('proxmox') || hostname.includes('pve')) { + return `http://${device.ip}:8006` + } + + // RTSP-прокси (go2rtc / mediamtx) + if ( + device.capabilities?.includes('rtsp') || + metaCls === 'rtsp_server' || + metaPlugin.includes('rtsp') + ) { + return `http://${device.ip}:1984` + } + + switch (device.category) { + case 'main_server': return `http://${device.ip}:80` + case 'router': return `http://${device.ip}:80` // MikroTik WebFig + case 'embedded': return `http://${device.ip}:5000` + case 'camera': return `http://${device.ip}:80` + default: return null + } +}