Implement DPELP batch run and result aggregation

Added backend and frontend support for running DPELP scenarios on all lines of a station and aggregating results. Introduced a new socket event 'run_all_dpelp', a helper for formatting line results, and logic to post results to a wiki endpoint. Also updated scenario command delays and improved overlay positioning logic in the UI.
This commit is contained in:
nguyentrungthat 2025-12-01 16:49:19 +07:00
parent 3e1ad11e72
commit 77027d4f8a
6 changed files with 185 additions and 30 deletions

View File

@ -6,6 +6,7 @@ import {
cleanData,
getLogWithTimeScenario,
isValidJson,
mapToLineFormat,
sleep,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
@ -15,6 +16,15 @@ import path from 'node:path'
import axios from 'axios'
import redis from '@adonisjs/redis/services/main'
type Inventory = {
pid: string
vid: string
sn: string
licenseLevel: string
licenseType: string
nextLicenseLevel: string
}
interface LineConfig {
id: number
port: number
@ -84,7 +94,7 @@ export default class LineConnection {
private outputInventory: string
private outputScenario: string
private bufferCommand: string
private retryConnect: number
public dataDPELP: string
constructor(config: LineConfig, socketIO: any) {
this.config = config
@ -97,7 +107,7 @@ export default class LineConnection {
this.outputInventory = ''
this.outputScenario = ''
this.bufferCommand = ''
this.retryConnect = 0
this.dataDPELP = 'No data'
}
connect(timeoutMs = 5000) {
@ -160,7 +170,7 @@ export default class LineConnection {
if (!this.config.inventory) {
setTimeout(() => {
this.getInventory()
}, 3000)
}, 5000)
}
appendLog(
cleanData(message),
@ -227,13 +237,7 @@ export default class LineConnection {
this.disconnect()
await sleep(2000)
await this.connect()
// await this.writeCommand(cmd)
// if (this.retryConnect <= 3) {
// console.log('Retry connect times', this.retryConnect)
// this.retryConnect += 1
// await this.connect()
// await this.writeCommand(cmd)
// }
return
}
@ -293,7 +297,7 @@ export default class LineConnection {
console.log(
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
)
if (script?.title === 'DPELP') this.dataDPELP = ''
this.isRunningScript = true
const now = Date.now()
this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`
@ -378,6 +382,15 @@ export default class LineConnection {
}
})
const detectLog = await this.detectLogWithAI(logScenarios)
const result = mapToLineFormat({
lineNumber: this.config.lineNumber,
inventory: this.config.inventory,
latestScenario: {
detectAI: detectLog,
},
data,
})
if (script?.title === 'DPELP') this.dataDPELP = result
if (this.config.latestScenario)
this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog }
this.config.data = data
@ -398,14 +411,6 @@ export default class LineConnection {
}
const step = steps[index]
this.outputScenario += `\n---send-command---"${step?.send ?? ''}"---${now}---\n`
appendLog(
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
let repeatCount = Number(step.repeat) || 1
const sendCommand = async () => {
if (repeatCount <= 0) {
@ -415,6 +420,14 @@ export default class LineConnection {
}
if (step.send) {
this.outputScenario += `\n---send-command---"${step?.send ?? ''}"---${now}---\n`
appendLog(
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
this.writeCommand(step?.send + '\r\n')
}

View File

@ -1,6 +1,20 @@
import fs from 'node:fs'
import path from 'node:path'
type DetectAI = {
status: string[]
issue: string[]
}
type InputData = {
lineNumber: number
inventory: any
latestScenario?: {
detectAI?: DetectAI
}
data?: any[]
}
/**
* Function to clean up unwanted characters from the output data.
* @param {string} data - The raw data to be cleaned.
@ -122,3 +136,54 @@ export function isValidJson(string: string) {
return false // Chuỗi không phải là định dạng JSON hợp lệ
}
}
export function mapToLineFormat(input: InputData): string {
const line = input.lineNumber
// Inventory info
const pid = input.inventory?.pid || ''
const vid = input.inventory?.vid || ''
const sn = input.inventory?.sn || ''
if (!pid || !sn) return `Line ${line}: No data`
// Find MAC address from "show version" or other sources
let mac = ''
const showVersion = input.data?.find((d) => d.command === 'show version')
if (showVersion?.textfsm?.[0]?.MAC_ADDRESS) {
mac = showVersion.textfsm[0].MAC_ADDRESS
}
// Get data license
const dataLicense = input.data?.find((comm: any) => comm.command?.trim() === 'show license')
const listLicense =
dataLicense?.textfsm && Array.isArray(dataLicense?.textfsm)
? dataLicense?.textfsm?.map((val: any) => val.FEATURE).join(', ')
: ''
const dataPlatform = input.data?.find((el) => el.command?.trim() === 'show platform')
const DPELP = dataPlatform && !dataPlatform?.output?.includes('Incomplete') ? true : false
// Detect AI issues
const issues = input.latestScenario?.detectAI?.issue || []
// Build output
let output = `Line ${line}: `
output += `PID: ${pid}, `
output += `VID: ${vid}, `
output += `SN: ${sn}, `
output += `Tested mode: ${DPELP ? 'DPELP' : 'DPEL'}, `
output += `${mac ? 'MAC Address:' + mac + `, ` : ''} `
output += `${listLicense ? 'License: ' + listLicense : ''}`
if (Array.isArray(issues) && issues.length > 0) {
output += `\n Issues:\n`
for (const issue of issues) {
output += ` - ${issue}\n`
}
} else if (typeof issues === 'string') {
output += `\n Issues: ${issues}`
}
return output.trim()
}

View File

@ -53,8 +53,26 @@ export const textfsmResults = (logContent: string, cmd: string) => {
// Parse commands and outputs
const matches = [...logContent.matchAll(regexPattern)]
const mergedMatches = []
for (let match of matches) {
const m = match
const command = m.groups?.command?.trim() || ''
if (command?.toLowerCase() === 'show version | include license') {
// Gộp vào phần tử trước
if (mergedMatches.length > 1) {
const lastMatch = mergedMatches[mergedMatches.length - 1]
if (lastMatch?.groups?.output) lastMatch.groups.output += '\n' + m?.groups?.output
}
continue
}
mergedMatches.push(m)
}
// Process matches
results = matches
results = mergedMatches
.map((match) => {
const command = match.groups?.command.trim() || ''
const output = match.groups?.output.trim() || ''

View File

@ -12,6 +12,7 @@ import APCController from '#services/apc_connection'
import { appendLog, cleanData, sleep } from '../app/ultils/helper.js'
import SwitchController from '#services/switch_connection'
import redis from '@adonisjs/redis/services/main'
import axios from 'axios'
interface HandleOptions {
command?: string
@ -531,6 +532,26 @@ export class WebSocketIo {
io.to(socket.id).emit('list_histories', result)
})
socket.on('run_all_dpelp', async (data) => {
const lineIds = data.lineIds
console.log('[DPELP] Received run all dpelp')
const results = await this.waitUntilAllReady(lineIds)
const d = new Date(Date.now())
const pad = (n: number) => String(n).padStart(2, '0')
const dataFormat =
`${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())}, ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
const linkWiki =
process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test'
await axios.post(linkWiki, {
data: `<pre>${results.join('\n\n')}\n</pre>`,
titleAuto: 'AUTO - ' + dataFormat,
})
})
})
socketServer.listen(SOCKET_IO_PORT, () => {
@ -1003,4 +1024,27 @@ export class WebSocketIo {
const items = await redis.zrange(key, 0, -1)
return items.map((i) => JSON.parse(i))
}
async waitUntilAllReady(lineIds: number[]) {
return new Promise<string[]>((resolve) => {
const interval = setInterval(() => {
let allReady = true
const results: string[] = []
for (const lineId of lineIds) {
const line = this.lineMap.get(lineId)
if (!line || !line.dataDPELP) {
allReady = false
break
}
results.push(line.dataDPELP)
}
if (allReady) {
clearInterval(interval)
console.log('[DPELP] All lines ready')
resolve(results)
}
}, 5000) // check mỗi 5 giây
})
}
}

View File

@ -761,6 +761,14 @@ const BottomToolBar = ({
isDisable={isDisable || selectedLines.length === 0}
onClick={() => {
// setSelectedLines([]);
if (
selectedLines.length > 0 &&
selectedLines.length === station?.lines?.length
) {
socket?.emit("run_all_dpelp", {
lineIds: selectedLines.map((line) => line.id),
});
}
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);

View File

@ -65,45 +65,52 @@ export const ButtonDPELP = ({
repeat: "1",
note: "",
},
{
expect: "",
send: "show version",
delay: "1000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show diag",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show post",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show env all",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show license",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show log",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},
{
expect: "",
send: "show platform",
delay: "1500",
delay: "3000",
repeat: "1",
note: "",
},