Pārlūkot izejas kodu

fix: generic 24-port fallback for missing inventory

clm0081 2 dienas atpakaļ
vecāks
revīzija
f31b257474

+ 1 - 1
docs/使用说明.md

@@ -242,7 +242,7 @@ zabbix-deploy/
     交换机名称最前面显示 `⚡️`;当前网络若没有 PoE 设备则不显示。
 - **节点标签**:`车间 · 楼层机柜U · 角色 · IP`;双击节点跳转该主机的问题页。
 - **告警/离线/延迟着色**:告警交换机整个节点填充红色,离线交换机同样整节点红色;延迟 **3–5ms 为橙色边框**,**大于 5ms 才为红色边框**。告警不会再只画一个红框。告警面板只显示拓扑内交换机,不混入服务器/测试机等“拓扑外”主机。
-- **显示终端(端口矩阵)**:勾选「显示终端」后,接入/汇聚交换机下方按实际端口清单绘制圆点;外圈颜色表示该端口**支持的最大速率**(100M 橙、1G 绿、2.5G 青、10G 紫、20G+ 深紫、未知灰),圆心实心颜色表示该端口**当前实际链路速率**。例如:10G 口插 1G 线 = 紫色外圈 + 绿色实心;10G 口未插线 = 空心紫色圈;2.5G 口 = 青色圈。端口较多时按实际面板自动分两行/多行,上联端口单独成行;历史快照没有端口清单时退回原来的终端圆点。端口颜色与上方链路速率颜色是两套语义,不要混用。
+- **显示终端(端口矩阵)**:勾选「显示终端」后,接入/汇聚交换机下方按实际端口清单绘制圆点;外圈颜色表示该端口**支持的最大速率**(100M 橙、1G 绿、2.5G 青、10G 紫、20G+ 深紫、未知灰),圆心实心颜色表示该端口**当前实际链路速率**。例如:10G 口插 1G 线 = 紫色外圈 + 绿色实心;10G 口未插线 = 空心紫色圈;2.5G 口 = 青色圈。端口较多时按实际面板自动分两行/多行,上联端口单独成行;历史快照没有端口清单时退回通用 24 口两行矩阵(12+12),不丢已有终端位置。端口颜色与上方链路速率颜色是两套语义,不要混用。
 - 右侧「离线交换机」面板可查看离线交换机并点击定位;终端面板点击任意终端可定位到其接入交换机及具体端口。
 - **时间轴**:右上「快照时间」下拉可选 `实时` 或历史某分钟快照(保存 **7 天**,5 分钟一个点)。
 - **明暗主题**:自动跟随 Zabbix 当前主题(Zabbix 深色即深色、亮色即亮色),无手动开关。

+ 40 - 15
web-modules/WecomCenter/assets/l2topology/app.js

@@ -774,6 +774,7 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
     if (showTerm) {
       w.all.forEach(function (n) {
         var rows = portRows(n.ports).length;
+        if (!rows) rows = 2; // generic 24-port fallback
         if (rows > portRowMax) portRowMax = rows;
       });
     }
@@ -1405,6 +1406,14 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
     }
   }
 
+  function portIndexFromName(name) {
+    var s = String(name || '').toUpperCase();
+    if (/^(ETH-TRUNK|VLANIF|NULL|INLOOPBACK|STACK-PORT|LOOPBACK|TUNNEL|METH|VBDIF)/.test(s)) return 0;
+    var m = s.match(/(\d+)(?!.*\d)/);
+    var n = m ? parseInt(m[1], 10) : 0;
+    return n > 0 && n <= 1024 ? n : 0;
+  }
+
   function addTerminals(L, byId, els) {
     termPortByMac = {};
     var bySw = {}, termBySwPort = {};
@@ -1415,28 +1424,44 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
       (bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
       if (tm.port) termBySwPort[tm.sw_ip + '|' + pkey(tm.port)] = tm;
     });
-    function pushTermDot(sw, p, tm, x, y, id) {
-      els.push({group: 'nodes', data: {
-        id: id, type: 'term', bg: terminalOnline(tm) ? '#2ECC71' : '#95a5a6',
-        mac: tm.mac, ip: tm.ip, online: terminalOnline(tm), speed: tm.speed,
-        sw_name: tm.sw_name, port: tm.port, label: '', termText: terminalText(tm)
-      }, position: {x: x, y: y}, selectable: false});
-      els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: id}});
-    }
     Object.keys(byId).forEach(function (sw) {
       var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
       var terms = bySw[sw] || [];
       var rawPorts = (p.node && p.node.ports) || [];
+      var ports = rawPorts.slice();
       if (!rawPorts.length) {
-        // Legacy/history snapshot without a port inventory: keep the compact
-        // terminal dots as a fallback instead of inventing unused ports.
-        terms.slice(0, 24).forEach(function (tm, k) {
-          var col = k % 12, row = Math.floor(k / 12);
-          pushTermDot(sw, p, tm, p.x - p.w / 2 + 16 + col * 9, p.y + 16 + row * 9, 't_' + tm.mac);
+        // The switch did not report a front-panel inventory (for example an
+        // old history snapshot).  Fall back to a generic 24-port access panel
+        // laid out as two rows of 12.  Terminals are placed into their real
+        // suffix position when the port name allows it; the remaining ports
+        // stay visible as unused hollow circles.
+        var used = {}, extra = [];
+        terms.forEach(function (tm) {
+          var idx = portIndexFromName(tm.port);
+          if (idx) used[idx] = tm; else extra.push(tm);
+        });
+        var maxIdx = 24;
+        Object.keys(used).forEach(function (k) { maxIdx = Math.max(maxIdx, parseInt(k, 10) || 0); });
+        ports = [];
+        for (var pi = 1; pi <= maxIdx; pi++) {
+          var ptm = used[pi];
+          var pspeed = ptm ? (parseFloat(ptm.speed) || 0) : 0;
+          ports.push({
+            name: ptm ? (ptm.port || ('GE1/0/' + pi)) : ('GE1/0/' + pi),
+            order: pi, speed: pspeed,
+            max: ptm ? portMaxFromName(ptm.port, pspeed) : 1000,
+            up: !!ptm, synthetic: true
+          });
+        }
+        extra.forEach(function (tm, k) {
+          var espeed = parseFloat(tm.speed) || 0;
+          ports.push({
+            name: tm.port || ('GE1/0/' + (maxIdx + k + 1)), order: 1000 + k,
+            speed: espeed, max: portMaxFromName(tm.port, espeed),
+            up: true, synthetic: true
+          });
         });
-        return;
       }
-      var ports = rawPorts.slice();
       var known = {};
       ports.forEach(function (pt) { known[pkey(pt.name)] = true; });
       // A terminal may sit on a logical interface (for example an Eth-Trunk)

+ 2 - 2
web-modules/WecomCenter/views/topology.view.php

@@ -6,7 +6,7 @@
 */
 use Modules\WecomCenter\Lib\I18n;
 
-$this->addCssFile('modules/WecomCenter/assets/l2topology/style.css?v=202611051950');
+$this->addCssFile('modules/WecomCenter/assets/l2topology/style.css?v=202611052015');
 $base = '/modules/WecomCenter/assets/l2topology/';
 
 $snapJson = (string) ($data['snap_json'] ?? 'null');
@@ -107,7 +107,7 @@ window.L2_META = {
 };
 </script>
 <script src="<?php echo $base; ?>cytoscape.min.js"></script>
-<script src="<?php echo $base; ?>app.js?v=202611051950"></script>
+<script src="<?php echo $base; ?>app.js?v=202611052015"></script>
 <script>
 (function(){
   var tabs = document.querySelectorAll('.side-tab');