Add summary-report debounce and small fixes

Backend: Add debounceSendSummaryReport to LineConnection and snapshotting to debounce sendReportSummary (10 min) to avoid duplicate summary emails; adapt sendReportSummary to accept a snapshot and use snapshot data when available. Reset physicalTest and listFeatureTested on line disconnect and add initConfig helper. Improve PoE/SFP parsing (filter names with '/' and match 'unknown') and adjust report HTML (tested/missing counts, column counts). Add PS_INCOMPATIBLE log rule. Comment out automatic wiki/email send in socket provider. Physical test service: suppress getFormReport call on completion and include PoE/SFP breakdown in the HTML report counts. Frontend: fix terminal open logic to check userOpenCLI instead of userEmailOpenCLI and display device MAC in the terminal modal.
This commit is contained in:
nguyentrungthat 2026-02-23 11:20:56 +07:00
parent f3dbd3d4cc
commit c644e798c6
6 changed files with 107 additions and 26 deletions

View File

@ -138,6 +138,7 @@ export default class LineConnection {
private debounceTimer: NodeJS.Timeout | null = null private debounceTimer: NodeJS.Timeout | null = null
private testingPortPoE: boolean private testingPortPoE: boolean
private outputTestingPortPoE: string private outputTestingPortPoE: string
private debounceSendSummaryReport: NodeJS.Timeout | null = null
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config this.config = config
@ -168,6 +169,7 @@ export default class LineConnection {
this.outputLoadIosLicense = '' this.outputLoadIosLicense = ''
this.listDeviceIos = [] this.listDeviceIos = []
this.debounceTimer = null this.debounceTimer = null
this.debounceSendSummaryReport = null
this.testingPortPoE = false this.testingPortPoE = false
this.outputTestingPortPoE = '' this.outputTestingPortPoE = ''
} }
@ -284,6 +286,8 @@ export default class LineConnection {
console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`) console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected' this.config.status = 'disconnected'
this.config.output += this.config.output + '[CLEAR_TERMINAL_SCROLL_BACK]' this.config.output += this.config.output + '[CLEAR_TERMINAL_SCROLL_BACK]'
this.config.listFeatureTested = []
this.physicalTest = new PhysicalPortTest([])
// this.config.inventory = undefined // this.config.inventory = undefined
this.socketIO.emit('line_disconnected', { this.socketIO.emit('line_disconnected', {
stationId, stationId,
@ -588,6 +592,18 @@ export default class LineConnection {
...new Set([...this.config.listFeatureTested, 'DPELP']), ...new Set([...this.config.listFeatureTested, 'DPELP']),
] ]
this.sendFeatureTested() this.sendFeatureTested()
// Debounce send summary report
if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport)
// Snapshot toàn bộ data tại thời điểm này
const snapshot = {
snapConfig: this.config,
snapPhysical: this.physicalTest,
}
this.debounceSendSummaryReport = setTimeout(() => {
this.sendReportSummary(snapshot)
}, 600000) // 10p debounce
// } // }
if (this.config.latestScenario) if (this.config.latestScenario)
this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog } this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog }
@ -1218,15 +1234,16 @@ export default class LineConnection {
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: "Gi1/0/1 auto off 0.0 n/a n/a 30.0 "
const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i) const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (matchPoE) { if (matchPoE) {
const name = matchPoE[1] const name = matchPoE[1]
ports.push(normalizeInterface(name)) if (name.includes('/')) ports.push(normalizeInterface(name))
} }
// Match: "Gi0/15 notconnect 1 auto auto 1000BaseSX SFP" // Match: "Gi0/15 notconnect 1 auto auto 1000BaseSX SFP"
// Match: "Gi0/16 notconnect 1 auto auto Not Present" // Match: "Gi0/16 notconnect 1 auto auto Not Present"
const matchSFP = line.match(/^([A-Za-z0-9\/]+).*\b(SFP|Not Present)\b/i) // Match: "Gi1/1/4 notconnect 1 auto auto unknown"
const matchSFP = line.match(/^([A-Za-z0-9\/]+).*\b(SFP|Not Present|unknown)\b/i)
if (matchSFP) { if (matchSFP) {
const name = matchSFP[1] const name = matchSFP[1]
ports.push(normalizeInterface(name) + ' (SFP)') ports.push(normalizeInterface(name) + ' (SFP)')
@ -1656,6 +1673,9 @@ ${log}
return '' return ''
} }
/**
* Check config RAM and Flash, if higher config will send report
*/
async checkConfigRam(mem: string, flash: string, pid: string, output: string) { async checkConfigRam(mem: string, flash: string, pid: string, output: string) {
const configRam = await detectConfigRamByModel(pid) const configRam = await detectConfigRamByModel(pid)
if (configRam) { if (configRam) {
@ -1687,6 +1707,9 @@ ${log}
} }
} }
/**
* Send list feature tested
*/
sendFeatureTested = async () => { sendFeatureTested = async () => {
this.socketIO.emit('feature_tested', { this.socketIO.emit('feature_tested', {
stationId: this.config.stationId, stationId: this.config.stationId,
@ -1695,35 +1718,47 @@ ${log}
}) })
} }
sendReportSummary = async () => { /**
const portPhysical = Array.from(this.physicalTest.ports.values()) * Send summary of all report (DPELP, Physical Testing)
*/
sendReportSummary = async (snapshot?: {
snapConfig: LineConfig
snapPhysical: PhysicalPortTest
}) => {
if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport)
const physicalTest = snapshot?.snapPhysical ? snapshot?.snapPhysical : this.physicalTest
const config = snapshot?.snapConfig ? snapshot?.snapConfig : this.config
const portPhysical = Array.from(physicalTest.ports.values())
const missing = portPhysical.filter((p) => !p.tested) const missing = portPhysical.filter((p) => !p.tested)
const missingPoE = missing.filter((p) => !p.name.includes('SFP')) const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
const missingSFP = missing.filter((p) => p.name.includes('SFP')) const missingSFP = missing.filter((p) => p.name.includes('SFP'))
const showVersion = this.config?.data?.find( const tested = portPhysical.filter((p) => p.tested)
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
const showVersion = config?.data?.find(
(d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver') (d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver')
) )
const dataShowVersion = const dataShowVersion =
showVersion?.textfsm && showVersion?.textfsm?.[0] showVersion?.textfsm && showVersion?.textfsm?.[0]
? showVersion?.textfsm?.[0] ? showVersion?.textfsm?.[0]
: this.config?.inventory : config?.inventory
const showLicense = this.config?.data?.find( const showLicense = config?.data?.find(
(d) => d.command?.trim()?.includes('show lic') || d.command?.trim()?.includes('sh lic') (d) => d.command?.trim()?.includes('show lic') || d.command?.trim()?.includes('sh lic')
) )
const dataShowLic = const dataShowLic =
showLicense?.textfsm && Array.isArray(showLicense?.textfsm) ? showLicense?.textfsm : null showLicense?.textfsm && Array.isArray(showLicense?.textfsm) ? showLicense?.textfsm : null
const issue = this.config?.latestScenario?.detectAI?.issue || [] const issue = config?.latestScenario?.detectAI?.issue || []
const summary = this.config?.latestScenario?.detectAI?.summary || '' const summary = config?.latestScenario?.detectAI?.summary || ''
const body = `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%"> const body = `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
<tr> <tr>
<td style="width: 50%; text-align: center;">DPELP</td> <td style="width: 600px; text-align: center;">DPELP</td>
<td style="text-align: center;">Physical Testing</td> <td style="text-align: center;">Physical Testing</td>
</tr> </tr>
<tr> <tr>
<td style="width: 50%;"> <td>
Model: <b>${this.config?.inventory?.pid ?? ''}</b> <b>${this.config?.inventory?.vid ?? ''}</b><br/> Model: <b>${config?.inventory?.pid ?? ''}</b> <b>${config?.inventory?.vid ?? ''}</b><br/>
Serial Number: <b>${this.config?.inventory?.sn ?? ''}</b><br/> Serial Number: <b>${config?.inventory?.sn ?? ''}</b><br/>
MAC: <b>${dataShowVersion?.MAC_ADDRESS ?? ''}</b><br/> MAC: <b>${dataShowVersion?.MAC_ADDRESS ?? ''}</b><br/>
IOS: <b>${dataShowVersion?.SOFTWARE_IMAGE ?? ''}</b> <b>${dataShowVersion?.VERSION ?? ''}</b><br/> IOS: <b>${dataShowVersion?.SOFTWARE_IMAGE ?? ''}</b> <b>${dataShowVersion?.VERSION ?? ''}</b><br/>
MEM: <b>${dataShowVersion?.MEMORY ? convertFromKilobytesString(dataShowVersion?.MEMORY) : ''}</b><br/> MEM: <b>${dataShowVersion?.MEMORY ? convertFromKilobytesString(dataShowVersion?.MEMORY) : ''}</b><br/>
@ -1741,14 +1776,14 @@ ${log}
</td> </td>
<td> <td>
Total Ports: ${portPhysical?.length}<br/> Total Ports: ${portPhysical?.length}<br/>
Ports Tested (Link UP): <b style="color: #008000;">${portPhysical.filter((p) => p.tested).length}</b><br/> Ports Tested (Link UP): <b style="color: #008000;">${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)</b><br/>
Ports Missing/Down: <b style="color: #ff0000;">${portPhysical.filter((p) => !p.tested).length}</b><br/> Ports Missing/Down: <b style="color: #ff0000;">${missing.length}</b><br/>
${ ${
missingPoE?.length missingPoE?.length
? ` ? `
<br/><b style="color: #ff0000;">Ports Missing PoE</b><br/> <br/><b style="color: #ff0000;">Ports Missing PoE</b><br/>
<br/> <br/>
<div style="column-count: 12;">${missingPoE.map((p) => this.physicalTest.normalizePortName(p.name)).join('<br/>')}</div> <div style="column-count: 6;">${missingPoE.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>
` `
: '' : ''
} }
@ -1757,7 +1792,7 @@ ${log}
? ` ? `
<br/><b style="color: #ff0000;">Ports Missing SFP</b><br/> <br/><b style="color: #ff0000;">Ports Missing SFP</b><br/>
<br/> <br/>
<div style="column-count: 12;">${missingSFP.map((p) => this.physicalTest.normalizePortName(p.name)).join('<br/>')}</div>` <div style="column-count: 6;">${missingSFP.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>`
: '' : ''
} }
</td> </td>
@ -1765,8 +1800,37 @@ ${log}
</table>` </table>`
await sendMessageToMail( await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Summary of Testing Results`, `[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
body body
) )
} }
/**
* Reset config information of line
*/
initConfig() {
this.config = {
id: 0,
port: 0,
lineNumber: 0,
ip: '',
stationId: 0,
stationName: '',
stationIp: '',
outlet: 0,
output: '',
status: '',
baud: 0,
openCLI: false,
userEmailOpenCLI: '',
userOpenCLI: '',
inventory: [],
data: [],
ports: [],
runningScenario: '',
runningPhysical: false,
listFeatureTested: [],
}
this.physicalTest = new PhysicalPortTest([])
}
} }

View File

@ -122,7 +122,7 @@ export class PhysicalPortTest {
} }
onDone() { onDone() {
this.getFormReport() // this.getFormReport()
// this.ports.clear() // this.ports.clear()
console.log('✅ Physical Test DONE') console.log('✅ Physical Test DONE')
} }
@ -175,7 +175,7 @@ export class PhysicalPortTest {
</td> </td>
<td> <td>
Total Ports: ${report.ports.length}<br/> Total Ports: ${report.ports.length}<br/>
Ports Tested (UP): <b style="color: #008000;">${tested.length}</b><br/> Ports Tested (UP): <b style="color: #008000;">${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)</b><br/>
Ports Missing: <b style="color: #ff0000;">${missing.length}</b><br/> Ports Missing: <b style="color: #ff0000;">${missing.length}</b><br/>
</td> </td>
</tr> </tr>

View File

@ -510,6 +510,13 @@ export const RULES: LogRule[] = [
level: 'WARN', level: 'WARN',
message: 'Hardware environment warning', message: 'Hardware environment warning',
}, },
{
id: 'PS_INCOMPATIBLE',
category: 'HARDWARE',
match: /%PLATFORM_FEP-\d+-FRU_PS_INCOMPATIBLE/i,
level: 'FAIL',
message: 'Power supply incompatible',
},
// ERROR // ERROR
{ {
id: 'MEMORY_ERROR', id: 'MEMORY_ERROR',

View File

@ -635,10 +635,10 @@ export class WebSocketIo {
console.error('Error sending wiki message:', error) console.error('Error sending wiki message:', error)
} }
try { try {
await sendMessageToMail( // await sendMessageToMail(
`[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`, // `[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`,
tableHTML // tableHTML
) // )
} catch (error) { } catch (error) {
console.error('Error sending mail:', error) console.error('Error sending mail:', error)
} }

View File

@ -601,7 +601,7 @@ function App() {
const openTerminal = (line: TLine) => { const openTerminal = (line: TLine) => {
setOpenModalTerminal(true); setOpenModalTerminal(true);
const data = { ...line }; const data = { ...line };
if (!line.userEmailOpenCLI) { if (!line.userOpenCLI) {
data.cliOpened = true; data.cliOpened = true;
data.userEmailOpenCLI = user?.email; data.userEmailOpenCLI = user?.email;
data.userOpenCLI = user?.userName; data.userOpenCLI = user?.userName;

View File

@ -923,6 +923,16 @@ const ModalTerminal = ({
: ""} : ""}
</Text> </Text>
</Flex> </Flex>
<Flex>
<Text size="md" mr={"sm"} fw={"bold"}>
MAC:
</Text>
<Text size="md">
{findDataShowVersion()
? findDataShowVersion()?.MAC_ADDRESS || ""
: ""}
</Text>
</Flex>
<Flex> <Flex>
<Text size="md" mr={"sm"} fw={"bold"}> <Text size="md" mr={"sm"} fw={"bold"}>
License: License: