From 63b264304ef9f4349139f71f51ca88660935291e Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:58:17 +0700 Subject: [PATCH] Update flow check physical ports test --- BACKEND/app/services/line_connection.ts | 77 ++++++++++--------- BACKEND/app/services/physical_test_service.ts | 61 ++++++++++++--- .../components/Components/AutoProgress.tsx | 47 +++++++++++ .../src/components/Modal/ModalTerminal.tsx | 50 +++++++++--- 4 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 FRONTEND/src/components/Components/AutoProgress.tsx diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 3782381..8e0394d 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -216,12 +216,6 @@ export default class LineConnection { } if (this.config.runningPhysical) { this.outputPhysicalTest += message - if (this.debounceTimer) clearTimeout(this.debounceTimer) - - if (this.testingPortPoE) - this.debounceTimer = setTimeout(() => { - this.flushLogBuffer() - }, 1000) // 1s debounce } if (data.toString().includes('More') || data.toString().includes('MORE')) this.writeCommand(' ') @@ -1108,35 +1102,39 @@ export default class LineConnection { return } - this.physicalTest.start(listPorts, this.config.inventory) - // const interval = setInterval(async () => { - // if (!this.physicalTest.done) { - // // const result = this.physicalTest.getResult() - // // console.warn('⚠️ Missing ports:', result.missingPorts) - // } else { - // clearInterval(interval) - // await this.sendReportPhysicalTest() - // this.endTesting() - // } - // }, 10000) + this.physicalTest.start( + listPorts.map((el) => el), + this.config.inventory + ) + const interval = setInterval(async () => { + if (!this.config.runningPhysical) { + clearInterval(interval) + } else { + this.flushLogBuffer() + } + }, 5000) } - flushLogBuffer() { - const lines = this.outputPhysicalTest.split(/\r?\n/) - - // giữ lại dòng cuối nếu chưa kết thúc hoàn chỉnh - this.outputPhysicalTest = lines.pop() || '' - - const completeLines = lines.join('\n') - - if (completeLines.trim()) { - const ports = this.physicalTest.handleLog(completeLines) - if (ports?.length) + async flushLogBuffer() { + try { + this.writeCommand('show power inline | include on\r\n') + this.writeCommand('\r\n') + await this.sleep(1000) + this.writeCommand('show interfaces status | include SFP\r\n') + this.writeCommand('\r\n') + await this.sleep(2000) + const output = this.outputPhysicalTest + this.outputPhysicalTest = '' + if (output) { + const ports = this.physicalTest.detectPorts(output) this.socketIO.emit('test_port_physical', { stationId: this.config.stationId, lineId: this.config.id, data: ports, }) + } + } catch (error) { + console.log('flushLogBuffer', error) } } @@ -1164,22 +1162,31 @@ export default class LineConnection { * Get list PoE ports */ async getPorts(): Promise { + this.writeCommand(' terminal length 0\r\n') this.writeCommand(' show power inline\r\n') this.writeCommand(' \r\n') - await this.sleep(5000) + await this.sleep(3000) + this.writeCommand(' show interfaces status\r\n') + this.writeCommand(' \r\n') + await this.sleep(4000) const statusOutput = this.outputPhysicalTest this.outputPhysicalTest = '' - const lines = statusOutput.split('\n') const ports = [] for (const line of lines) { // Match: "Gi0/1 is up, line protocol is up" - const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i) - - if (match) { - const name = match[1] + const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i) + if (matchPoE) { + const name = matchPoE[1] ports.push(normalizeInterface(name)) } + // Match: "Gi0/15 notconnect 1 auto auto 1000BaseSX SFP" + // Match: "Gi0/16 notconnect 1 auto auto Not Present" + const matchSFP = line.match(/^([A-Za-z0-9\/]+).*\b(SFP|Not Present)\b/i) + if (matchSFP) { + const name = matchSFP[1] + ports.push(normalizeInterface(name) + ' (SFP)') + } } this.config.ports = [...new Set(ports)] return [...new Set(ports)] @@ -1191,7 +1198,7 @@ export default class LineConnection { async sendReportPhysicalTest() { const formReport = this.physicalTest.getFormReport() await sendMessageToMail( - `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Port Test`, + `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Ports Test`, formReport ) } diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts index e8983e4..c609d54 100644 --- a/BACKEND/app/services/physical_test_service.ts +++ b/BACKEND/app/services/physical_test_service.ts @@ -3,7 +3,8 @@ import { normalizeInterface } from '../ultils/helper.js' import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js' const LINK_REGEX = /Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i -const POE_GRANTED_REGEX = /%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i +const POE_GRANTED_REGEX = + /.*%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i const POE_DISCONNECT_REGEX = /%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i @@ -64,6 +65,7 @@ export class PhysicalPortTest { match = line.match(POE_GRANTED_REGEX) if (match) { iface = normalizeInterface(match[1]) + state = 'up' markTested = true } @@ -159,7 +161,7 @@ export class PhysicalPortTest { const status = missing.length === 0 ? 'PASS' : 'WARNING' return ` - Physical Port Test Report
+ Physical Ports Test Report
@@ -169,26 +171,25 @@ export class PhysicalPortTest { Total Ports : ${report.ports.length}
- Ports Tested (UP) : ${tested.length}
- Ports Missing : ${missing.length}
+ Ports Tested (UP) : ${tested.length}
+ Ports Missing : ${missing.length}

────────────────────────────────
- Passed Ports
+ Passed Ports
${ tested.length ? `
${tested.map((p) => this.normalizePortName(p.name)).join('
')}

` : '' } -
${ missing.length ? ` ────────────────────────────────
- Missing Ports
+ Missing Ports
${missing.map((p) => this.normalizePortName(p.name)).join('
')}

` : '' @@ -209,7 +210,11 @@ export class PhysicalPortTest { if (!port) return '' // Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2" - const match = port.match(/^([A-Za-z]+)([\d/]+)$/) + const isSFP = port.includes('SFP') + const match = port + .replace('(SFP)', '') + .trim() + .match(/^([A-Za-z]+)([\d/]+)$/) if (!match) return port @@ -221,6 +226,44 @@ export class PhysicalPortTest { const last = parts[parts.length - 1] const preLast = parts?.length > 1 ? parts[parts.length - 2] : '' - return `${type?.slice(0, 2)}${preLast ? preLast + '/' : ''}${last}` + return `${type?.slice(0, 2)}${preLast ? preLast + '/' : ''}${last}${isSFP ? ' (SFP)' : ''}` + } + + /** + * Function 1: Lấy danh sách các cổng có module SFP từ lệnh 'show interfaces status' + * Logic: Tìm dòng bắt đầu bằng Tên Port và có chứa từ khóa "SFP" ở cuối. + * Function 2: Lấy danh sách các cổng đang cấp nguồn (PoE on) từ lệnh 'show power inline' + * Logic: Tìm dòng bắt đầu bằng Tên Port, cột tiếp theo là Admin status, cột tiếp theo là 'on'. + */ + detectPorts(output: string): string[] { + for (const line of output.split('\n')) { + if (line?.includes('include')) continue + const ports: string[] = [] + const regexPoE = /^([A-Za-z0-9\/]+).*\on\b/i + const regexSFP = /^([A-Za-z0-9\/]+).*\bSFP\b/i + + let matchPoE = line.match(regexPoE) + if (matchPoE) { + ports.push(matchPoE[1]) + } + let matchSFP = line.match(regexSFP) + if (matchSFP) { + ports.push(matchSFP[1] + ' (SFP)') + } + if (ports.length > 0) { + ports + .filter((el) => el) + .forEach((el) => { + const iface = normalizeInterface(el) + const port = this.ports.get(iface) + if (port) { + port.lastState = 'up' + port.tested = true + } + }) + } + } + + return this.getTestedPorts() } } diff --git a/FRONTEND/src/components/Components/AutoProgress.tsx b/FRONTEND/src/components/Components/AutoProgress.tsx new file mode 100644 index 0000000..d743317 --- /dev/null +++ b/FRONTEND/src/components/Components/AutoProgress.tsx @@ -0,0 +1,47 @@ +import { Progress } from "@mantine/core"; +import { useEffect, useRef, useState } from "react"; + +interface AutoProgressProps { + ms: number; // thời gian chạy từ 0 -> 100 + start: boolean; + style?: React.CSSProperties; +} + +export default function AutoProgress({ ms, start, style }: AutoProgressProps) { + const [value, setValue] = useState(0); + const intervalRef = useRef(null); + + useEffect(() => { + // cleanup cũ + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + if (!start) { + setValue(0); + return; + } + + const stepTime = 50; // update mỗi 50ms + const stepValue = 100 / (ms / stepTime); + + intervalRef.current = setInterval(() => { + setValue((prev) => { + if (prev + stepValue >= 100) { + return 0; // reset và chạy vòng mới + } + return prev + stepValue; + }); + }, stepTime); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [start, ms]); + + return ; +} diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx index ae547ce..76f3b83 100644 --- a/FRONTEND/src/components/Modal/ModalTerminal.tsx +++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx @@ -52,6 +52,7 @@ import ModalSelectIOS from "./ModalSelectIOS"; import ModalSelectLicense from "./ModalSelectLicense"; import ModalRunScenario from "./ModalRunScenario"; import DrawerScenario from "./ModalScenario"; +import AutoProgress from "../Components/AutoProgress"; const apiUrl = import.meta.env.VITE_BACKEND_URL; const INIT_TICKET = { @@ -115,6 +116,11 @@ const ModalTerminal = ({ const [openSelectLicense, setOpenSelectLicense] = useState(false); const [openScenarioModal, setOpenScenarioModal] = useState(false); const [openDrawerScenario, setOpenDrawerScenario] = useState(false); + const [isPhysicalTest, setIsPhysicalTest] = useState(false); + + useEffect(() => { + setIsPhysicalTest(line?.runningPhysical || false); + }, [line?.runningPhysical]); useEffect(() => { if (opened && line?.tickets && line?.tickets?.length > 0) { @@ -437,7 +443,10 @@ const ModalTerminal = ({ if (!port) return ""; // Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2" - const match = port.match(/^([A-Za-z]+)([\d/]+)$/); + const match = port + .replace("(SFP)", "") + .trim() + .match(/^([A-Za-z]+)([\d/]+)$/); if (!match) return port; @@ -1034,16 +1043,17 @@ const ModalTerminal = ({ -
+ +
List ports{" "} - {line?.runningPhysical + {isPhysicalTest ? `(${line?.listPortsPhysical?.length || 0}/${ line?.ports?.length || 0 })` @@ -1053,10 +1063,10 @@ const ModalTerminal = ({ - {line?.runningPhysical && line?.ports + {isPhysicalTest && line?.ports ? line.ports.map((port, i) => ( {normalizePortName(port)} + {port?.includes("SFP") ? ( + + SFP + + ) : ( + "" + )} )) : null} @@ -1074,7 +1100,7 @@ const ModalTerminal = ({ - {line?.runningPhysical ? ( + {isPhysicalTest ? (