diff --git a/frontend/src/pages/DeviceDetail.tsx b/frontend/src/pages/DeviceDetail.tsx index a2b8639..44ae1a6 100644 --- a/frontend/src/pages/DeviceDetail.tsx +++ b/frontend/src/pages/DeviceDetail.tsx @@ -574,6 +574,9 @@ export function DeviceDetailPage() { {device.rack_name && {device.rack_name}} {device.vendor && {device.vendor}} {device.model && {device.model}} + {device.capabilities.map((c) => ( + {c} + ))} diff --git a/frontend/src/pages/InventoryObjectDetail.tsx b/frontend/src/pages/InventoryObjectDetail.tsx index 3406462..c0deb97 100644 --- a/frontend/src/pages/InventoryObjectDetail.tsx +++ b/frontend/src/pages/InventoryObjectDetail.tsx @@ -60,6 +60,30 @@ 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 @@ -337,15 +361,15 @@ export function InventoryObjectDetailPage() { ) } const d = row as DeviceRow - return ( - - navigate(`/inventory/objects/${objId}/devices/${d.id}`)} - > - {d.ip} - - + const webUrl = getDeviceWebUrl(d) + return webUrl ? ( + + + {d.ip} + + + ) : ( + {d.ip} ) }, }, @@ -354,8 +378,16 @@ export function InventoryObjectDetailPage() { dataIndex: 'hostname', key: 'hostname', onCell: (row: TableRow) => row._type === 'group' ? { colSpan: 0 } : {}, - render: (_: unknown, row: TableRow) => - row._type === 'group' ? null : ((row as DeviceRow).hostname ?? '—'), + render: (_: unknown, row: TableRow) => { + if (row._type === 'group') return null + const d = row as DeviceRow + if (!d.hostname) return + return ( + navigate(`/inventory/objects/${objId}/devices/${d.id}`)}> + {d.hostname} + + ) + }, }, { title: 'Стойка / локация', @@ -365,9 +397,8 @@ export function InventoryObjectDetailPage() { if (row._type === 'group') return null const d = row as DeviceRow if (d.rack_name) return {rackLabel(d.rack_name, d.rack_type)} - const loc = (d as any).location - if (loc === 'server_room') return Серверная - if (loc === 'street') return Улица + if (d.location === 'server_room') return Серверная + if (d.location === 'street') return Улица return }, },