Update flow check physical ports test

This commit is contained in:
nguyentrungthat 2026-02-03 13:58:17 +07:00
parent d33878c112
commit 63b264304e
4 changed files with 179 additions and 56 deletions

View File

@ -216,12 +216,6 @@ export default class LineConnection {
} }
if (this.config.runningPhysical) { if (this.config.runningPhysical) {
this.outputPhysicalTest += message 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')) if (data.toString().includes('More') || data.toString().includes('MORE'))
this.writeCommand(' ') this.writeCommand(' ')
@ -1108,35 +1102,39 @@ export default class LineConnection {
return return
} }
this.physicalTest.start(listPorts, this.config.inventory) this.physicalTest.start(
// const interval = setInterval(async () => { listPorts.map((el) => el),
// if (!this.physicalTest.done) { this.config.inventory
// // const result = this.physicalTest.getResult() )
// // console.warn('⚠️ Missing ports:', result.missingPorts) const interval = setInterval(async () => {
// } else { if (!this.config.runningPhysical) {
// clearInterval(interval) clearInterval(interval)
// await this.sendReportPhysicalTest() } else {
// this.endTesting() this.flushLogBuffer()
// } }
// }, 10000) }, 5000)
} }
flushLogBuffer() { async flushLogBuffer() {
const lines = this.outputPhysicalTest.split(/\r?\n/) try {
this.writeCommand('show power inline | include on\r\n')
// giữ lại dòng cuối nếu chưa kết thúc hoàn chỉnh this.writeCommand('\r\n')
this.outputPhysicalTest = lines.pop() || '' await this.sleep(1000)
this.writeCommand('show interfaces status | include SFP\r\n')
const completeLines = lines.join('\n') this.writeCommand('\r\n')
await this.sleep(2000)
if (completeLines.trim()) { const output = this.outputPhysicalTest
const ports = this.physicalTest.handleLog(completeLines) this.outputPhysicalTest = ''
if (ports?.length) if (output) {
const ports = this.physicalTest.detectPorts(output)
this.socketIO.emit('test_port_physical', { this.socketIO.emit('test_port_physical', {
stationId: this.config.stationId, stationId: this.config.stationId,
lineId: this.config.id, lineId: this.config.id,
data: ports, data: ports,
}) })
}
} catch (error) {
console.log('flushLogBuffer', error)
} }
} }
@ -1164,22 +1162,31 @@ export default class LineConnection {
* Get list PoE ports * Get list PoE ports
*/ */
async getPorts(): Promise<string[]> { async getPorts(): Promise<string[]> {
this.writeCommand(' terminal length 0\r\n')
this.writeCommand(' show power inline\r\n') this.writeCommand(' show power inline\r\n')
this.writeCommand(' \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 const statusOutput = this.outputPhysicalTest
this.outputPhysicalTest = '' this.outputPhysicalTest = ''
const lines = statusOutput.split('\n') const lines = statusOutput.split('\n')
const ports = [] const ports = []
for (const line of lines) { for (const line of lines) {
// Match: "Gi0/1 is up, line protocol is up" // Match: "Gi0/1 is up, line protocol is up"
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i) const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (matchPoE) {
if (match) { const name = matchPoE[1]
const name = match[1]
ports.push(normalizeInterface(name)) 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)] this.config.ports = [...new Set(ports)]
return [...new Set(ports)] return [...new Set(ports)]
@ -1191,7 +1198,7 @@ export default class LineConnection {
async sendReportPhysicalTest() { async sendReportPhysicalTest() {
const formReport = this.physicalTest.getFormReport() const formReport = this.physicalTest.getFormReport()
await sendMessageToMail( 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 formReport
) )
} }

View File

@ -3,7 +3,8 @@ import { normalizeInterface } from '../ultils/helper.js'
import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js' import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
const LINK_REGEX = 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 /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 = const POE_DISCONNECT_REGEX =
/%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i /%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) match = line.match(POE_GRANTED_REGEX)
if (match) { if (match) {
iface = normalizeInterface(match[1]) iface = normalizeInterface(match[1])
state = 'up'
markTested = true markTested = true
} }
@ -159,7 +161,7 @@ export class PhysicalPortTest {
const status = missing.length === 0 ? 'PASS' : 'WARNING' const status = missing.length === 0 ? 'PASS' : 'WARNING'
return ` return `
<b>Physical Port Test Report</b><br/> <b>Physical Ports Test Report</b><br/>
<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%"> <table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
<tr> <tr>
<td style="width: 50%;"> <td style="width: 50%;">
@ -169,26 +171,25 @@ export class PhysicalPortTest {
</td> </td>
<td> <td>
Total Ports : ${report.ports.length}<br/> Total Ports : ${report.ports.length}<br/>
Ports Tested (UP) : ${tested.length}<br/> Ports Tested (UP) : <b style="color: #008000;">${tested.length}</b><br/>
Ports Missing : ${missing.length}<br/> Ports Missing : <b style="color: #ff0000;">${missing.length}</b><br/>
</td> </td>
</tr> </tr>
</table> </table>
<br/> <br/>
<br/> <br/>
<b>Passed Ports</b><br/> <b style="color: #008000;">Passed Ports</b><br/>
${ ${
tested.length tested.length
? `<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${tested.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/> ? `<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${tested.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
` `
: '' : ''
} }
<br/>
${ ${
missing.length missing.length
? ` ? `
<br/> <br/>
<b>Missing Ports</b><br/> <b style="color: #ff0000;">Missing Ports</b><br/>
<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${missing.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/> <div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${missing.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
` `
: '' : ''
@ -209,7 +210,11 @@ export class PhysicalPortTest {
if (!port) return '' if (!port) return ''
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2" // 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 if (!match) return port
@ -221,6 +226,44 @@ export class PhysicalPortTest {
const last = parts[parts.length - 1] const last = parts[parts.length - 1]
const preLast = parts?.length > 1 ? parts[parts.length - 2] : '' 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 module SFP từ lệnh 'show interfaces status'
* Logic: Tìm dòng bắt đu bằng Tên Port 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 Admin status, cột tiếp theo '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()
} }
} }

View File

@ -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<NodeJS.Timeout | null>(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 <Progress style={style} value={value} animated />;
}

View File

@ -52,6 +52,7 @@ import ModalSelectIOS from "./ModalSelectIOS";
import ModalSelectLicense from "./ModalSelectLicense"; import ModalSelectLicense from "./ModalSelectLicense";
import ModalRunScenario from "./ModalRunScenario"; import ModalRunScenario from "./ModalRunScenario";
import DrawerScenario from "./ModalScenario"; import DrawerScenario from "./ModalScenario";
import AutoProgress from "../Components/AutoProgress";
const apiUrl = import.meta.env.VITE_BACKEND_URL; const apiUrl = import.meta.env.VITE_BACKEND_URL;
const INIT_TICKET = { const INIT_TICKET = {
@ -115,6 +116,11 @@ const ModalTerminal = ({
const [openSelectLicense, setOpenSelectLicense] = useState<boolean>(false); const [openSelectLicense, setOpenSelectLicense] = useState<boolean>(false);
const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false); const [openScenarioModal, setOpenScenarioModal] = useState<boolean>(false);
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false); const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
const [isPhysicalTest, setIsPhysicalTest] = useState<boolean>(false);
useEffect(() => {
setIsPhysicalTest(line?.runningPhysical || false);
}, [line?.runningPhysical]);
useEffect(() => { useEffect(() => {
if (opened && line?.tickets && line?.tickets?.length > 0) { if (opened && line?.tickets && line?.tickets?.length > 0) {
@ -437,7 +443,10 @@ const ModalTerminal = ({
if (!port) return ""; if (!port) return "";
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2" // 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; if (!match) return port;
@ -1034,16 +1043,17 @@ const ModalTerminal = ({
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="physical"> <Tabs.Panel value="physical">
<fieldset <AutoProgress
style={{ start={isPhysicalTest || false}
marginTop: "12px", ms={5000}
}} style={{ marginTop: "8px" }}
> />
<fieldset>
<legend> <legend>
<Flex> <Flex>
<Text> <Text>
List ports{" "} List ports{" "}
{line?.runningPhysical {isPhysicalTest
? `(${line?.listPortsPhysical?.length || 0}/${ ? `(${line?.listPortsPhysical?.length || 0}/${
line?.ports?.length || 0 line?.ports?.length || 0
})` })`
@ -1053,10 +1063,10 @@ const ModalTerminal = ({
</legend> </legend>
<ScrollArea h={"45vh"} p={"4px"}> <ScrollArea h={"45vh"} p={"4px"}>
<SimpleGrid cols={5} spacing={"xs"}> <SimpleGrid cols={5} spacing={"xs"}>
{line?.runningPhysical && line?.ports {isPhysicalTest && line?.ports
? line.ports.map((port, i) => ( ? line.ports.map((port, i) => (
<Text <Text
fz={"16px"} fz={"15px"}
key={i} key={i}
c={ c={
line?.listPortsPhysical?.includes(port) line?.listPortsPhysical?.includes(port)
@ -1065,6 +1075,22 @@ const ModalTerminal = ({
} }
> >
{normalizePortName(port)} {normalizePortName(port)}
{port?.includes("SFP") ? (
<Text
mt={-4}
fz={"11px"}
key={i}
c={
line?.listPortsPhysical?.includes(port)
? "#014a1a"
: "#dedede"
}
>
SFP
</Text>
) : (
""
)}
</Text> </Text>
)) ))
: null} : null}
@ -1074,7 +1100,7 @@ const ModalTerminal = ({
<Flex justify={"space-between"}> <Flex justify={"space-between"}>
<Button <Button
fw={400} fw={400}
disabled={isDisable || !line?.runningPhysical} disabled={isDisable || !isPhysicalTest}
variant="outline" variant="outline"
color="yellow" color="yellow"
size="xs" size="xs"
@ -1094,7 +1120,7 @@ const ModalTerminal = ({
> >
Reset Reset
</Button> </Button>
{line?.runningPhysical ? ( {isPhysicalTest ? (
<Button <Button
fw={400} fw={400}
disabled={isDisable} disabled={isDisable}
@ -1133,7 +1159,7 @@ const ModalTerminal = ({
setIsDisable(true); setIsDisable(true);
setTimeout(() => { setTimeout(() => {
setIsDisable(false); setIsDisable(false);
}, 10000); }, 15000);
}} }}
> >
Start Physical Test Start Physical Test