diff --git a/frontend/src/router/routes/network.js b/frontend/src/router/routes/network.js
index a7eea5f..c721ac7 100644
--- a/frontend/src/router/routes/network.js
+++ b/frontend/src/router/routes/network.js
@@ -5,7 +5,7 @@ export default [
{
path: 'network',
name: 'network',
- component: () => import('../../views/network/NetworkDevicesList.vue'),
+ component: () => import('../../views/network/NetworkHub.vue'),
meta: { plugin: 'network' }
},
{
@@ -27,10 +27,9 @@ export default [
meta: { requiresAuth: true, plugin: 'network' }
},
{
+ // Legacy path -> the Networks tab of the hub.
path: 'networks',
- name: 'networks',
- component: () => import('../../views/network/SubnetsBrowse.vue'),
- meta: { plugin: 'network' }
+ redirect: { path: '/network', query: { tab: 'networks' } }
},
{
path: 'networks/:id',
diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue
index a0427b6..e101527 100644
--- a/frontend/src/views/AppLayout.vue
+++ b/frontend/src/views/AppLayout.vue
@@ -156,8 +156,7 @@ const defaultNav = [
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10 },
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
- { name: 'Network Devices', icon: 'network-wired', route: '/network', position: 18 },
- { name: 'View Networks', icon: 'globe', route: '/networks', position: 19 },
+ { name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },
diff --git a/frontend/src/views/network/NetworkHub.vue b/frontend/src/views/network/NetworkHub.vue
new file mode 100644
index 0000000..089900e
--- /dev/null
+++ b/frontend/src/views/network/NetworkHub.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/network/SubnetDetail.vue b/frontend/src/views/network/SubnetDetail.vue
index e317b53..6975044 100644
--- a/frontend/src/views/network/SubnetDetail.vue
+++ b/frontend/src/views/network/SubnetDetail.vue
@@ -37,14 +37,13 @@
| Device | IP | Type |
-
+
|
-
- {{ dev.name || dev.hostname || dev.assetnumber }}
-
+ {{ dev.name || dev.assetnumber }}
+ {{ dev.name || dev.assetnumber }}
|
{{ dev.ipaddress }} |
- {{ dev.networkdevicetypename || '-' }} |
+ {{ typeLabel(dev.assettype) }} |
@@ -67,6 +66,14 @@ const loading = ref(true)
const devices = computed(() => subnet.value?.devices || [])
+const TYPE_LABELS = {
+ computer: 'PC', network_device: 'Network Device', printer: 'Printer',
+ machine: 'Machine', measuring_tool: 'Measuring Tool',
+}
+function typeLabel(type) {
+ return TYPE_LABELS[type] || type || '-'
+}
+
async function load() {
loading.value = true
try {
diff --git a/frontend/src/views/network/SubnetsBrowse.vue b/frontend/src/views/network/SubnetsBrowse.vue
index 562e6eb..ad2fa4e 100644
--- a/frontend/src/views/network/SubnetsBrowse.vue
+++ b/frontend/src/views/network/SubnetsBrowse.vue
@@ -1,11 +1,7 @@
-
-
-
+
diff --git a/plugins/network/api/routes.py b/plugins/network/api/routes.py
index 7f09f14..751ae65 100644
--- a/plugins/network/api/routes.py
+++ b/plugins/network/api/routes.py
@@ -831,31 +831,34 @@ def get_subnet(subnet_id: int):
def _devices_in_subnet(cidr):
- """Network devices (with their primary IP) whose IP is inside cidr. Matching
- is done in Python because a device's IP lives in a Communication row, not a
- column, so it cannot be a simple SQL join."""
+ """ANY asset (PC, printer, network device, ...) whose primary IP falls in
+ cidr. A subnet is cross-type, so this matches on the core Communication +
+ Asset tables, not just network devices. Matching is in Python because the IP
+ lives in a Communication row, not a column."""
import ipaddress
+ from shopdb.api import Asset, AssetType, Communication
try:
net = ipaddress.ip_network(cidr, strict=False)
except (ValueError, TypeError):
return []
+ rows = (Communication.query
+ .join(Asset, Communication.assetid == Asset.assetid)
+ .join(AssetType, Asset.assettypeid == AssetType.assettypeid)
+ .filter(Communication.isprimary == True,
+ Communication.ipaddress.isnot(None))
+ .with_entities(Asset.assetid, Asset.assetnumber, Asset.name,
+ AssetType.assettype, Communication.ipaddress).all())
result = []
- for netdev in NetworkDevice.query.all():
- ip = _primary_ip(netdev.assetid)
+ for assetid, assetnumber, name, assettype, ip in rows:
+ ip = (ip or '').strip()
if not ip:
continue
try:
- if ipaddress.ip_address(ip.strip()) in net:
- asset = netdev.asset
+ if ipaddress.ip_address(ip) in net:
result.append({
- 'networkdeviceid': netdev.networkdeviceid,
- 'assetid': netdev.assetid,
- 'assetnumber': asset.assetnumber if asset else None,
- 'name': asset.name if asset else None,
- 'hostname': netdev.hostname,
- 'ipaddress': ip,
- 'networkdevicetypename': (netdev.networkdevicetype.networkdevicetype
- if netdev.networkdevicetype else None),
+ 'assetid': assetid, 'assetnumber': assetnumber, 'name': name,
+ 'assettype': assettype, 'ipaddress': ip,
+ 'url': _asset_detail_url(assettype, assetid),
})
except (ValueError, TypeError):
continue
@@ -863,6 +866,27 @@ def _devices_in_subnet(cidr):
return result
+def _asset_detail_url(assettype, assetid):
+ """Best-effort front-end detail path for an asset by type. Resolves the
+ per-type extension id lazily/guarded (subnet listings span plugins); returns
+ None when the plugin is absent so the frontend just shows the row."""
+ try:
+ if assettype == 'network_device':
+ row = NetworkDevice.query.filter_by(assetid=assetid).first()
+ return f'/network/{row.networkdeviceid}' if row else None
+ if assettype == 'printer':
+ from plugins.printers.models import Printer
+ row = Printer.query.filter_by(assetid=assetid).first()
+ return f'/printers/{row.printerid}' if row else None
+ if assettype == 'computer':
+ from plugins.computers.models import Computer
+ row = Computer.query.filter_by(assetid=assetid).first()
+ return f'/pcs/{row.computerid}' if row else None
+ except ImportError:
+ return None
+ return None
+
+
@network_bp.route('/subnets', methods=['POST'])
@jwt_required()
@require_permission('network.create')
diff --git a/plugins/network/plugin.py b/plugins/network/plugin.py
index 3b29c64..903129c 100644
--- a/plugins/network/plugin.py
+++ b/plugins/network/plugin.py
@@ -208,17 +208,11 @@ class NetworkPlugin(BasePlugin):
"""Return navigation menu items."""
return [
{
- 'name': 'Network Devices',
+ 'name': 'Network',
'icon': 'network-wired',
'route': '/network',
'position': 18,
},
- {
- 'name': 'View Networks',
- 'icon': 'globe',
- 'route': '/networks',
- 'position': 19,
- },
]
def get_permissions(self) -> List: