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 —
},
},