This commit is contained in:
dv 2026-04-09 11:29:20 +03:00
parent 87bbd2844c
commit 0d0cec1f9f
3 changed files with 56 additions and 24 deletions

View file

@ -4,6 +4,7 @@ import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
EyeOutlined, EyeOutlined,
LinkOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { import {
Badge, Badge,
@ -29,6 +30,7 @@ import { useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { useAlerts, useAcknowledgeAlert, useResolveAlert, type AlertItem } from '../api/alerts' import { useAlerts, useAcknowledgeAlert, useResolveAlert, type AlertItem } from '../api/alerts'
import { useDevice, useDeleteDevice, useObject } from '../api/objects' import { useDevice, useDeleteDevice, useObject } from '../api/objects'
import { getDeviceWebUrl } from '../utils/deviceUrl'
import { useSnapshots, useSnapshotDiff } from '../api/snapshots' import { useSnapshots, useSnapshotDiff } from '../api/snapshots'
import { useDeviceTasks, useTask, type Task } from '../api/tasks' import { useDeviceTasks, useTask, type Task } from '../api/tasks'
import { DeviceEditModal } from '../components/DeviceEditModal' import { DeviceEditModal } from '../components/DeviceEditModal'
@ -397,6 +399,7 @@ export function DeviceDetailPage() {
const effectiveStatus = liveStatus?.status ?? device?.status ?? 'unknown' const effectiveStatus = liveStatus?.status ?? device?.status ?? 'unknown'
const effectivePing = liveStatus !== null ? liveStatus.ping_ms : (device?.last_ping_ms ?? null) const effectivePing = liveStatus !== null ? liveStatus.ping_ms : (device?.last_ping_ms ?? null)
const webUrl = device ? getDeviceWebUrl(device) : null
const handleDelete = async () => { const handleDelete = async () => {
try { try {
@ -581,6 +584,18 @@ export function DeviceDetailPage() {
</Col> </Col>
<Col> <Col>
<Space> <Space>
{webUrl && (
<Tooltip title={webUrl}>
<Button
icon={<LinkOutlined />}
href={webUrl}
target="_blank"
rel="noopener noreferrer"
>
Веб-интерфейс
</Button>
</Tooltip>
)}
<Button icon={<EditOutlined />} onClick={() => setEditOpen(true)}> <Button icon={<EditOutlined />} onClick={() => setEditOpen(true)}>
Редактировать Редактировать
</Button> </Button>

View file

@ -56,34 +56,11 @@ import { CategoryTag, DeviceStatusBadge } from '../components/StatusBadge'
import { DeviceEditModal } from '../components/DeviceEditModal' import { DeviceEditModal } from '../components/DeviceEditModal'
import { DiscoveryDrawer } from '../components/DiscoveryDrawer' import { DiscoveryDrawer } from '../components/DiscoveryDrawer'
import { stripEmpty } from '../utils/form' import { stripEmpty } from '../utils/form'
import { getDeviceWebUrl } from '../utils/deviceUrl'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const SERVER_CATEGORIES = new Set(['main_server', 'vm', 'router']) 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 => const rackLabel = (name: string, type: string | null | undefined): string =>
type ? `${name} ${type}` : name type ? `${name} ${type}` : name
const DEVICE_COL_SPAN = 8 const DEVICE_COL_SPAN = 8

View file

@ -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
}
}