Jelajahi Sumber

feat(l2): terminal search/details and readable full-topology spacing

clm0081 3 hari lalu
induk
melakukan
0c8292c39e
38 mengubah file dengan 375 tambahan dan 59 penghapusan
  1. 121 20
      web-modules/WecomCenter/assets/l2topology/app.js
  2. 4 1
      web-modules/WecomCenter/assets/l2topology/style.css
  3. 7 1
      web-modules/WecomCenter/locale/bg_BG.php
  4. 7 1
      web-modules/WecomCenter/locale/ca_ES.php
  5. 7 1
      web-modules/WecomCenter/locale/cs_CZ.php
  6. 7 1
      web-modules/WecomCenter/locale/da_DK.php
  7. 7 1
      web-modules/WecomCenter/locale/de_DE.php
  8. 7 1
      web-modules/WecomCenter/locale/el_GR.php
  9. 7 1
      web-modules/WecomCenter/locale/en.php
  10. 7 1
      web-modules/WecomCenter/locale/en_GB.php
  11. 7 1
      web-modules/WecomCenter/locale/es_ES.php
  12. 7 1
      web-modules/WecomCenter/locale/fa_IR.php
  13. 7 1
      web-modules/WecomCenter/locale/fi_FI.php
  14. 7 1
      web-modules/WecomCenter/locale/fr_FR.php
  15. 7 1
      web-modules/WecomCenter/locale/he_IL.php
  16. 7 1
      web-modules/WecomCenter/locale/hu_HU.php
  17. 7 1
      web-modules/WecomCenter/locale/id_ID.php
  18. 7 1
      web-modules/WecomCenter/locale/it_IT.php
  19. 7 1
      web-modules/WecomCenter/locale/ja_JP.php
  20. 7 1
      web-modules/WecomCenter/locale/ka_GE.php
  21. 7 1
      web-modules/WecomCenter/locale/ko_KR.php
  22. 7 1
      web-modules/WecomCenter/locale/lt_LT.php
  23. 7 1
      web-modules/WecomCenter/locale/lv_LV.php
  24. 7 1
      web-modules/WecomCenter/locale/nb_NO.php
  25. 7 1
      web-modules/WecomCenter/locale/nl_NL.php
  26. 7 1
      web-modules/WecomCenter/locale/pl_PL.php
  27. 7 1
      web-modules/WecomCenter/locale/pt_BR.php
  28. 7 1
      web-modules/WecomCenter/locale/ro_RO.php
  29. 7 1
      web-modules/WecomCenter/locale/ru_RU.php
  30. 7 1
      web-modules/WecomCenter/locale/sk_SK.php
  31. 7 1
      web-modules/WecomCenter/locale/sv_SE.php
  32. 7 1
      web-modules/WecomCenter/locale/tr_TR.php
  33. 7 1
      web-modules/WecomCenter/locale/uk_UA.php
  34. 7 1
      web-modules/WecomCenter/locale/uz_UZ.php
  35. 7 1
      web-modules/WecomCenter/locale/vi_VN.php
  36. 7 1
      web-modules/WecomCenter/locale/zh_CN.php
  37. 7 1
      web-modules/WecomCenter/locale/zh_TW.php
  38. 5 3
      web-modules/WecomCenter/views/topology.view.php

+ 121 - 20
web-modules/WecomCenter/assets/l2topology/app.js

@@ -30,6 +30,7 @@
   var dark = false;
   var showTerm = false;
   var onlyOff = false;
+  var pinnedTerm = null;
   var paused = false;
   var countdown = 0;
   var timer = null;
@@ -455,7 +456,8 @@
   var FLOOR_LEFT = 1130, FLOOR_RIGHT = 1525;
   var CHART_W = 1560;
   var SEC_HEAD = 46, CORE_ROW = 92, WORK_TOP = 148;
-  var ROW_H = 24, ROW_GAP = 3, WS_GAP = 10, SEC_GAP = 34;
+  var ROW_H = 24, ROW_GAP = 8, WS_GAP = 20, SEC_GAP = 40;
+  var FLOOR_GAP = 18;
 
   function makeNetworks() {
     var raw = CFG.networks;
@@ -654,7 +656,7 @@
   }
 
   function layoutWorkshop(w, top) {
-    var rowH = ROW_H + (showTerm ? 24 : 0);
+    var rowH = ROW_H + ROW_GAP + (showTerm ? 24 : 0);
     var floorMap = {}, floors = [];
     w.access.forEach(function (n) {
       var fi = floorInfo(n);
@@ -666,10 +668,10 @@
     var accessH = 0;
     floors.forEach(function (f) {
       f.nodes.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)); });
-      f.h = 22 + f.nodes.length * rowH + 6;
+      f.h = 24 + f.nodes.length * rowH + 8;
       accessH += f.h;
     });
-    if (floors.length > 1) accessH += (floors.length - 1) * 5;
+    if (floors.length > 1) accessH += (floors.length - 1) * FLOOR_GAP;
     var distH = w.dists.length ? (w.dists.length * 64 + (w.dists.length - 1) * 8) : 0;
     var inner = Math.max(accessH, distH, 54);
     var h = 38 + inner + 10;
@@ -682,7 +684,7 @@
       f.nodes.forEach(function (n, i) {
         f.rows.push({id: n.id, x: ACCESS_X, y: cursor + 22 + rowH / 2 + i * rowH});
       });
-      cursor -= 5;
+      cursor -= FLOOR_GAP;
     });
     var dstart = top + 38 + (inner - distH) / 2;
     w.distPos = w.dists.map(function (n, i) {
@@ -819,6 +821,12 @@
     if (s >= 1000) return (s / 1000) + 'G';
     return s + 'M';
   }
+  function terminalOnline(tm) {
+    return tm && (tm.online === true || tm.online === 1 || tm.online === '1' || tm.online === 'true');
+  }
+  function terminalText(tm) {
+    return tm.mac + '\n' + speedText(tm.speed);
+  }
 
   /* ---------------- style ---------------- */
   function styleFor(d) {
@@ -918,7 +926,13 @@
       }},
       {selector: 'node[type="term"]', style: {
         'width': 7, 'height': 7, 'shape': 'ellipse', 'background-color': 'data(bg)',
-        'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '',
+        'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3',
+        'label': 'data(label)', 'font-size': 9, 'font-weight': 700,
+        'color': d ? '#eaf1fb' : '#182c44',
+        'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 4,
+        'text-wrap': 'wrap', 'text-max-width': 160,
+        'text-background-color': d ? '#0d1b2a' : '#ffffff', 'text-background-opacity': 0.9,
+        'text-background-padding': '2px',
         'z-index-compare': 'manual', 'z-index': 30
       }},
       {selector: 'edge[type="tilelink"]', style: {
@@ -1037,6 +1051,7 @@
     buildLegend();
     buildAlarms(snap);
     buildOffline(snap);
+    buildTerminals(snap);
   }
 
   function renderFull(snap) {
@@ -1084,15 +1099,21 @@
       }, position: {x: p.x, y: p.y}, classes: cls.join(' ')});
     });
 
-    // Every protocol-discovered link = one straight segment. Endpoints sit at
-    // the label left-edge midpoint; the labels paint on top, so a parent line
-    // starts under the parent label and ends exactly at the child's left edge.
+    // Every protocol-discovered link = one straight segment.  Cytoscape's
+    // percentage endpoints are unreliable with preset labels, so use two
+    // invisible anchor nodes placed exactly on the label left-edge midpoint.
+    // The labels paint on top, so a parent line starts under the parent label
+    // and ends exactly at the child's left edge.
     pathParent = buildParents(snap, L);
     (snap.edges || []).forEach(function (e, i) {
-      if (!byId[e.source] || !byId[e.target]) return;
+      var ps = byId[e.source], pt = byId[e.target];
+      if (!ps || !pt) return;
+      var sp = 'sp_' + i, tp = 'tp_' + i;
+      els.push({group: 'nodes', data: {id: sp, type: 'edgepoint'}, position: {x: ps.x - ps.w / 2, y: ps.y}, selectable: false, grabbable: false});
+      els.push({group: 'nodes', data: {id: tp, type: 'edgepoint'}, position: {x: pt.x - pt.w / 2, y: pt.y}, selectable: false, grabbable: false});
       var col = speedColor(e.speed, e.state);
       els.push({group: 'edges', data: {
-        id: 'e_' + i, type: 'link', source: e.source, target: e.target, p: e.source, c: e.target,
+        id: 'e_' + i, type: 'link', source: sp, target: tp, p: e.source, c: e.target,
         speed: e.speed, state: e.state, a_port: e.a_port, b_port: e.b_port,
         color: col, ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid', w: Math.min(4, speedWidth(e.speed))
       }});
@@ -1110,18 +1131,31 @@
     var bySw = {};
     ((lastSnap && lastSnap.terminals) || []).forEach(function (tm) {
       if (!byId[tm.sw_ip]) return;
-      if (onlyOff && tm.online) return;
+      if (onlyOff && terminalOnline(tm)) return;
       (bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
     });
     Object.keys(bySw).forEach(function (sw) {
       var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
-      bySw[sw].sort(function (a, b) { return (a.online === b.online) ? 0 : (a.online ? -1 : 1); });
+      bySw[sw].sort(function (a, b) { return (terminalOnline(a) === terminalOnline(b)) ? 0 : (terminalOnline(a) ? -1 : 1); });
+      // A search hit is pinned into the visible 24 so any MAC in the snapshot
+      // can be located even when its switch has more than 24 terminals.
+      if (pinnedTerm) {
+        for (var pi = 0; pi < bySw[sw].length; pi++) {
+          if (bySw[sw][pi].mac === pinnedTerm) {
+            if (pi > 0) { var pin = bySw[sw].splice(pi, 1)[0]; bySw[sw].unshift(pin); }
+            break;
+          }
+        }
+      }
       bySw[sw].slice(0, 24).forEach(function (tm, k) {
         var col = k % 12, row = Math.floor(k / 12);
         var x = p.x - p.w / 2 + 16 + col * 9;
         var y = p.y + 16 + row * 9;
-        els.push({group: 'nodes', data: {id: 't_' + tm.mac, type: 'term', bg: tm.online ? '#2ECC71' : '#95a5a6', mac: tm.mac, ip: tm.ip, online: tm.online},
-          position: {x: x, y: y}, selectable: false});
+        els.push({group: 'nodes', data: {
+          id: 't_' + tm.mac, 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: 't_' + tm.mac}});
       });
     });
@@ -1339,14 +1373,14 @@
   }
 
   function buildOffline(snap) {
-    var off = (snap.terminals || []).filter(function (tm) { return !tm.online; });
+    var off = (snap.terminals || []).filter(function (tm) { return !terminalOnline(tm); });
     el('l2-offline-count').textContent = off.length;
     if (!off.length) { el('l2-offline-list').innerHTML = '<div class="panel-empty">' + t('l2.offline_empty', 'No offline terminals.') + '</div>'; return; }
     var h = '';
     off.forEach(function (tm) {
       h += '<div class="off-row" data-mac="' + esc(tm.mac) + '">' +
         '<div class="off-main"><div class="off-mac">' + esc(tm.mac) + '</div>' +
-        '<div class="off-sub">' + esc(tm.sw_name || tm.sw_ip) + ' · ' + esc(tm.port || '') + '</div></div></div>';
+        '<div class="off-sub">' + esc(speedText(tm.speed)) + ' · ' + esc(tm.sw_name || tm.sw_ip) + ' · ' + esc(tm.port || '') + '</div></div></div>';
     });
     var box = el('l2-offline-list');
     box.innerHTML = h;
@@ -1358,6 +1392,39 @@
     });
   }
 
+  function buildTerminals(snap) {
+    var list = (snap.terminals || []).filter(function (tm) { return !onlyOff || !terminalOnline(tm); });
+    if (el('l2-term-count')) el('l2-term-count').textContent = list.length;
+    var box = el('l2-term-list');
+    if (!box) return;
+    if (!showTerm) {
+      box.innerHTML = '<div class="panel-empty">' + t('l2.show_term', 'Show terminals') + '</div>';
+      return;
+    }
+    if (!list.length) {
+      box.innerHTML = '<div class="panel-empty">' + t('l2.no_data', 'No data') + '</div>';
+      return;
+    }
+    list.sort(function (a, b) {
+      var x = String(a.sw_name || a.sw_ip || '').localeCompare(String(b.sw_name || b.sw_ip || ''));
+      return x || String(a.mac).localeCompare(String(b.mac));
+    });
+    var h = '';
+    list.forEach(function (tm) {
+      var on = terminalOnline(tm);
+      h += '<div class="off-row term-row" data-mac="' + esc(tm.mac) + '">' +
+        '<span class="term-dot" style="background:' + (on ? '#2ECC71' : '#95a5a6') + '"></span>' +
+        '<div class="off-main"><div class="off-mac">' + esc(tm.mac) + '</div>' +
+        '<div class="off-sub"><span class="term-rate">' + esc(speedText(tm.speed)) + '</span> · ' +
+        (on ? t('l2.term_on', 'online') : t('l2.term_off', 'offline')) + ' · ' +
+        esc(tm.sw_name || tm.sw_ip || '') + ' · ' + esc(tm.port || '') + '</div></div></div>';
+    });
+    box.innerHTML = h;
+    box.querySelectorAll('.term-row').forEach(function (r) {
+      r.addEventListener('click', function () { revealById('t_' + r.getAttribute('data-mac')); });
+    });
+  }
+
   function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]; }); }
 
   /* ---------------- interactions ---------------- */
@@ -1383,10 +1450,25 @@
     if (id.indexOf('t_') === 0) {
       var tm = (lastSnap.terminals || []).filter(function (x) { return 't_' + x.mac === id; })[0];
       if (!tm) return;
+      pinnedTerm = tm.mac;
       showTerm = true;
       if (el('l2-showterm')) el('l2-showterm').checked = true;
+      // A search hit must be visible even when the offline-only filter is on.
+      if (onlyOff && terminalOnline(tm)) {
+        onlyOff = false;
+        if (el('l2-onlyoff')) el('l2-onlyoff').checked = false;
+      }
       if (!cy || cy.getElementById(id).empty()) render(lastSnap);
-      setTimeout(function () { focusNode(id); }, 90);
+      setTimeout(function () {
+        // Center the switch that the terminal hangs under, then flash the
+        // terminal dot so the exact port location is obvious.
+        focusNode(tm.sw_ip || id);
+        var tn = cy && cy.getElementById(id);
+        if (tn && tn.length) {
+          tn.addClass('hl');
+          setTimeout(function () { tn.removeClass('hl'); }, 2600);
+        }
+      }, 90);
     } else {
       focusNode(id);
     }
@@ -1443,6 +1525,20 @@
       showTip(e, 'Link ' + esc(d.a_port || '') + ' ↔ ' + esc(d.b_port || '') + '<br>' + sp + ' · ' + esc(d.state || ''));
     });
     cy.on('mouseout', 'edge', hideTip);
+    cy.on('mouseover', 'node[type="term"]', function (e) {
+      var d = e.target.data();
+      e.target.data('label', d.termText || '');
+      showTip(e, '<b>' + esc(t('l2.term_title', 'Terminal')) + ' ' + esc(d.mac || '') + '</b><br>' +
+        esc(t('l2.term_mac', 'MAC address')) + ': ' + esc(d.mac || '') + '<br>' +
+        esc(t('l2.term_ip', 'IP address')) + ': ' + esc(d.ip || '—') + '<br>' +
+        esc(t('l2.term_rate', 'Link rate')) + ': ' + esc(speedText(d.speed)) + '<br>' +
+        esc(t('l2.term_switch', 'Switch')) + ': ' + esc(d.sw_name || '—') + '<br>' +
+        esc(t('l2.term_port', 'Port')) + ': ' + esc(d.port || '—'));
+    });
+    cy.on('mouseout', 'node[type="term"]', function (e) {
+      e.target.data('label', '');
+      hideTip();
+    });
     cy.on('tap', 'node[type="switch"]', function (e) {
       lockedPath = e.target.id();
       highlightPath(lockedPath);
@@ -1476,7 +1572,9 @@
   function buildIndex(snap) {
     var idx = [];
     (snap.nodes || []).forEach(function (n) { idx.push({id: n.id, label: n.name, sub: n.ip, kind: 'sw'}); });
-    (snap.terminals || []).forEach(function (tm) { idx.push({id: 't_' + tm.mac, label: tm.ip || tm.mac, sub: tm.mac + ' · ' + (tm.sw_name || ''), kind: 'term', mac: tm.mac}); });
+    (snap.terminals || []).forEach(function (tm) {
+      idx.push({id: 't_' + tm.mac, label: tm.mac, sub: (tm.ip || '') + ' · ' + speedText(tm.speed) + ' · ' + (tm.sw_name || ''), kind: 'term', mac: tm.mac});
+    });
     return idx;
   }
   var INDEX = [];
@@ -1485,9 +1583,12 @@
     q = (q || '').trim().toLowerCase();
     if (!q) { box.style.display = 'none'; box.innerHTML = ''; return; }
     var hits = [];
+    var qn = q.replace(/[^a-z0-9]/g, '');
     for (var i = 0; i < INDEX.length && hits.length < 14; i++) {
       var it = INDEX[i];
-      if ((it.label && it.label.toLowerCase().indexOf(q) >= 0) || (it.sub && it.sub.toLowerCase().indexOf(q) >= 0)) hits.push(it);
+      var hay = ((it.label || '') + ' ' + (it.sub || '')).toLowerCase();
+      var hayN = hay.replace(/[^a-z0-9]/g, '');
+      if (hay.indexOf(q) >= 0 || (qn && hayN.indexOf(qn) >= 0)) hits.push(it);
     }
     if (!hits.length) { box.innerHTML = '<div class="sug-empty">—</div>'; box.style.display = 'block'; return; }
     box.innerHTML = hits.map(function (h) {

+ 4 - 1
web-modules/WecomCenter/assets/l2topology/style.css

@@ -129,13 +129,16 @@
 .panel-empty { padding: 22px 14px; color: var(--muted); text-align: center; font-size: 13px; }
 
 .alarm-row, .off-row { display: flex; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border); cursor: pointer; }
-.alarm-row:hover, .off-row:hover { background: var(--panel-2); }
+.alarm-row:hover, .off-row:hover, .term-row:hover { background: var(--panel-2); }
 .alarm-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; margin-top: 4px; }
 .alarm-main, .off-main { min-width: 0; }
 .alarm-title { font-size: 12.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
 .alarm-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
 .off-mac { font-size: 12.5px; font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--text); }
 .off-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
+.term-row { align-items: flex-start; }
+.term-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; }
+.term-rate { color: var(--brand); font-weight: 700; }
 
 /* tooltip */
 .l2-tip {

+ 7 - 1
web-modules/WecomCenter/locale/bg_BG.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - bg_BG (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ca_ES.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ca_ES (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/cs_CZ.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - cs_CZ (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/da_DK.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - da_DK (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/de_DE.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - de_DE
 return [
 	'btn.import' => 'Vorlage importieren',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/el_GR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - el_GR (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/en.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - English (base language). Copy this file to <lang>.php to add a language.
 return [
 	'menu.title'      => 'ROMAN Custom Center',
@@ -236,6 +236,12 @@ return [
 	'l2.lat_norm'        => 'Normal',
 	'l2.term_on'         => 'Online',
 	'l2.term_off'        => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in'         => 'Zoom in',
 	'l2.zoom_out'        => 'Zoom out',
 	'l2.snapshot'        => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/en_GB.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - en_GB (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/es_ES.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - es_ES
 return [
 	'btn.import' => 'Importar plantilla',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/fa_IR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - fa_IR (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/fi_FI.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - fi_FI (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/fr_FR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - fr_FR
 return [
 	'btn.import' => 'Importer le modèle',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/he_IL.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - he_IL (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/hu_HU.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - hu_HU (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/id_ID.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - id_ID (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/it_IT.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - it_IT
 return [
 	'btn.import' => 'Importa template',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ja_JP.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ja_JP
 return [
 	'btn.import' => 'テンプレートをインポート',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ka_GE.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ka_GE (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ko_KR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ko_KR
 return [
 	'btn.import' => '템플릿 가져오기',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/lt_LT.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - lt_LT (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/lv_LV.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - lv_LV (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/nb_NO.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - nb_NO (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/nl_NL.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - nl_NL (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/pl_PL.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - pl_PL
 return [
 	'btn.import' => 'Importuj szablon',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/pt_BR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - pt_BR
 return [
 	'btn.import' => 'Importar modelo',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ro_RO.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ro_RO (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/ru_RU.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - ru_RU
 return [
 	'btn.import' => 'Импорт шаблона',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/sk_SK.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - sk_SK (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/sv_SE.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - sv_SE (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/tr_TR.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - tr_TR
 return [
 	'btn.import' => 'Şablonu içe aktar',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/uk_UA.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - uk_UA (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/uz_UZ.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - uz_UZ (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/vi_VN.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - vi_VN (not translated yet, falls back to English)
 return [	'menu.title' => 'ROMAN Custom Center',
 	'page.title' => 'ROMAN Custom Center',
@@ -195,6 +195,12 @@ return [	'menu.title' => 'ROMAN Custom Center',
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => 'Terminal',
+	'l2.term_mac' => 'MAC address',
+	'l2.term_rate' => 'Link rate',
+	'l2.term_switch' => 'Switch',
+	'l2.term_port' => 'Port',
+	'l2.term_ip' => 'IP address',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 7 - 1
web-modules/WecomCenter/locale/zh_CN.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - 简体中文
 return [
 	'menu.title'      => 'ROMAN 定制中心',
@@ -223,6 +223,12 @@ return [
 	'l2.lat_norm'        => '正常',
 	'l2.term_on'         => '在线',
 	'l2.term_off'        => '离线',
+	'l2.term_title' => '终端',
+	'l2.term_mac' => 'MAC 地址',
+	'l2.term_rate' => '连接速率',
+	'l2.term_switch' => '交换机',
+	'l2.term_port' => '端口',
+	'l2.term_ip' => 'IP 地址',
 	'l2.zoom_in'         => '放大',
 	'l2.zoom_out'        => '缩小',
 	'l2.snapshot'        => '快照',

+ 7 - 1
web-modules/WecomCenter/locale/zh_TW.php

@@ -1,4 +1,4 @@
-<?php
+<?php
 // WeCom Center - zh_TW
 return [
 	'btn.import' => '匯入範本',
@@ -206,6 +206,12 @@ return [
 	'l2.lat_norm' => 'Normal',
 	'l2.term_on' => 'Online',
 	'l2.term_off' => 'Offline',
+	'l2.term_title' => '終端',
+	'l2.term_mac' => 'MAC 位址',
+	'l2.term_rate' => '連線速率',
+	'l2.term_switch' => '交換機',
+	'l2.term_port' => '埠',
+	'l2.term_ip' => 'IP 位址',
 	'l2.zoom_in' => 'Zoom in',
 	'l2.zoom_out' => 'Zoom out',
 	'l2.snapshot' => 'Snapshot',

+ 5 - 3
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=20261105');
+$this->addCssFile('modules/WecomCenter/assets/l2topology/style.css?v=20261106');
 $base = '/modules/WecomCenter/assets/l2topology/';
 
 $snapJson = (string) ($data['snap_json'] ?? 'null');
@@ -19,7 +19,7 @@ $i18nKeys = [
 	'l2.refresh_now','l2.fit','l2.alarms','l2.offline','l2.time_live','l2.time_btn',
 	'l2.time_label','l2.no_data','l2.offline_empty','l2.alarm_empty','l2.updated',
 	'l2.pause','l2.resume','l2.legend_speed','l2.legend_tier','l2.legend_lat','l2.core','l2.main','l2.dist','l2.access',
-	'l2.lat_norm','l2.term_on','l2.term_off','l2.zoom_in','l2.zoom_out','l2.snapshot','l2.showing','l2.live_tag',
+	'l2.lat_norm','l2.term_on','l2.term_off','l2.term_title','l2.term_mac','l2.term_rate','l2.term_switch','l2.term_port','l2.term_ip','l2.zoom_in','l2.zoom_out','l2.snapshot','l2.showing','l2.live_tag',
 	'l2.unit_sw','l2.tile_hint','l2.not_in_topo','l2.click_hint','l2.floor_na',
 	'l2.net_surve','l2.net_office','l2.net_other'
 ];
@@ -74,10 +74,12 @@ if (!function_exists('l2e')) { function l2e($v) { return htmlspecialchars((strin
       <div class="side-tabs">
         <div class="side-tab active" data-sec="alarms"><?php echo l2e(I18n::t('l2.alarms')); ?> <span class="badge" id="l2-alarm-count">0</span></div>
         <div class="side-tab" data-sec="offline"><?php echo l2e(I18n::t('l2.offline')); ?> <span class="badge" id="l2-offline-count" style="background:#78909c">0</span></div>
+        <div class="side-tab" data-sec="terms"><?php echo l2e(I18n::t('l2.show_term')); ?> <span class="badge" id="l2-term-count" style="background:#2E75B6">0</span></div>
       </div>
       <div class="side-body">
         <div class="panel-sec active" id="sec-alarms"><div id="l2-alarm-list"></div></div>
         <div class="panel-sec" id="sec-offline"><div id="l2-offline-list"></div></div>
+        <div class="panel-sec" id="sec-terms"><div id="l2-term-list"></div></div>
       </div>
       <div class="l2-legend" id="l2-legend"></div>
     </div>
@@ -100,7 +102,7 @@ window.L2_META = {
 };
 </script>
 <script src="<?php echo $base; ?>cytoscape.min.js"></script>
-<script src="<?php echo $base; ?>app.js?v=20261105"></script>
+<script src="<?php echo $base; ?>app.js?v=20261106"></script>
 <script>
 (function(){
   var tabs = document.querySelectorAll('.side-tab');