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 testingPortPoE: boolean
private outputTestingPortPoE: string
private debounceSendSummaryReport: NodeJS.Timeout | null = null
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config
@ -168,6 +169,7 @@ export default class LineConnection {
this.outputLoadIosLicense = ''
this.listDeviceIos = []
this.debounceTimer = null
this.debounceSendSummaryReport = null
this.testingPortPoE = false
this.outputTestingPortPoE = ''
}
@ -284,6 +286,8 @@ export default class LineConnection {
console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected'
this.config.output += this.config.output + '[CLEAR_TERMINAL_SCROLL_BACK]'
this.config.listFeatureTested = []
this.physicalTest = new PhysicalPortTest([])
// this.config.inventory = undefined
this.socketIO.emit('line_disconnected', {
stationId,
@ -588,6 +592,18 @@ export default class LineConnection {
...new Set([...this.config.listFeatureTested, 'DPELP']),
]
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)
this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog }
@ -1218,15 +1234,16 @@ export default class LineConnection {
const lines = statusOutput.split('\n')
const ports = []
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)
if (matchPoE) {
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/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) {
const name = matchSFP[1]
ports.push(normalizeInterface(name) + ' (SFP)')
@ -1656,6 +1673,9 @@ ${log}
return ''
}
/**
* Check config RAM and Flash, if higher config will send report
*/
async checkConfigRam(mem: string, flash: string, pid: string, output: string) {
const configRam = await detectConfigRamByModel(pid)
if (configRam) {
@ -1687,6 +1707,9 @@ ${log}
}
}
/**
* Send list feature tested
*/
sendFeatureTested = async () => {
this.socketIO.emit('feature_tested', {
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 missingPoE = 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')
)
const dataShowVersion =
showVersion?.textfsm && 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')
)
const dataShowLic =
showLicense?.textfsm && Array.isArray(showLicense?.textfsm) ? showLicense?.textfsm : null
const issue = this.config?.latestScenario?.detectAI?.issue || []
const summary = this.config?.latestScenario?.detectAI?.summary || ''
const issue = config?.latestScenario?.detectAI?.issue || []
const summary = config?.latestScenario?.detectAI?.summary || ''
const body = `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
<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>
</tr>
<tr>
<td style="width: 50%;">
Model: <b>${this.config?.inventory?.pid ?? ''}</b> <b>${this.config?.inventory?.vid ?? ''}</b><br/>
Serial Number: <b>${this.config?.inventory?.sn ?? ''}</b><br/>
<td>
Model: <b>${config?.inventory?.pid ?? ''}</b> <b>${config?.inventory?.vid ?? ''}</b><br/>
Serial Number: <b>${config?.inventory?.sn ?? ''}</b><br/>
MAC: <b>${dataShowVersion?.MAC_ADDRESS ?? ''}</b><br/>
IOS: <b>${dataShowVersion?.SOFTWARE_IMAGE ?? ''}</b> <b>${dataShowVersion?.VERSION ?? ''}</b><br/>
MEM: <b>${dataShowVersion?.MEMORY ? convertFromKilobytesString(dataShowVersion?.MEMORY) : ''}</b><br/>
@ -1741,14 +1776,14 @@ ${log}
</td>
<td>
Total Ports: ${portPhysical?.length}<br/>
Ports Tested (Link UP): <b style="color: #008000;">${portPhysical.filter((p) => p.tested).length}</b><br/>
Ports Missing/Down: <b style="color: #ff0000;">${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;">${missing.length}</b><br/>
${
missingPoE?.length
? `
<br/><b style="color: #ff0000;">Ports Missing PoE</b><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/>
<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>
@ -1765,8 +1800,37 @@ ${log}
</table>`
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
)
}
/**
* 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() {
this.getFormReport()
// this.getFormReport()
// this.ports.clear()
console.log('✅ Physical Test DONE')
}
@ -175,7 +175,7 @@ export class PhysicalPortTest {
</td>
<td>
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/>
</td>
</tr>

View File

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

View File

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

View File

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

View File

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