2564 lines
104 KiB
TypeScript
2564 lines
104 KiB
TypeScript
import fs from 'node:fs'
|
||
import { textfsmResults } from './../ultils/templates/index.js'
|
||
import net from 'node:net'
|
||
import {
|
||
appendLog,
|
||
buildBody,
|
||
canInputCommand,
|
||
classifyLog,
|
||
cleanData,
|
||
convertFromKilobytesString,
|
||
detectConfigRamByModel,
|
||
detectScenarioByModel,
|
||
escapeHtml,
|
||
isRamSufficient,
|
||
isValidJson,
|
||
LogStreamBuffer,
|
||
mapErrorsToRows,
|
||
mapToLineFormat,
|
||
normalizeInterface,
|
||
parseLicenseReport,
|
||
sendMessageToMail,
|
||
sleep,
|
||
TestSession,
|
||
updateNoteToERP,
|
||
} from '../ultils/helper.js'
|
||
import Scenario from '#models/scenario'
|
||
import path from 'node:path'
|
||
import axios from 'axios'
|
||
import redis from '@adonisjs/redis/services/main'
|
||
import Line from '#models/line'
|
||
import PromptAi from '#models/prompt_ai'
|
||
import { CustomSocket, ErrorRow, TestResult } from '../ultils/types.js'
|
||
import momentTZ from 'moment-timezone'
|
||
import { PhysicalPortTest } from './physical_test_service.js'
|
||
import Station from '#models/station'
|
||
import IosLicenseController from '#controllers/ios_license_controller'
|
||
|
||
type Inventory = {
|
||
pid: string
|
||
vid: string
|
||
sn: string
|
||
licenseLevel: string
|
||
licenseType: string
|
||
nextLicenseLevel: string
|
||
}
|
||
|
||
interface LineConfig {
|
||
id: number
|
||
port: number
|
||
lineNumber: number
|
||
ip: string
|
||
stationId: number
|
||
stationName: string
|
||
stationIp: string
|
||
apcName?: string
|
||
outlet: number
|
||
output: string
|
||
status: string
|
||
baud: number
|
||
openCLI: boolean
|
||
userEmailOpenCLI: string
|
||
userOpenCLI: string
|
||
inventory: any
|
||
latestScenario?: {
|
||
name: string
|
||
time: number
|
||
detectAI?: {
|
||
status: string[]
|
||
issue: string[]
|
||
summary: string
|
||
}
|
||
}
|
||
data: {
|
||
command: string
|
||
output: string
|
||
textfsm: string
|
||
}[]
|
||
ports: string[]
|
||
runningScenario: string
|
||
runningPhysical: boolean
|
||
listFeatureTested: string[]
|
||
isReady: boolean
|
||
isSkipPhysical?: boolean
|
||
reasonSkipPhysical?: string
|
||
// history: string
|
||
}
|
||
|
||
/** HISTORY
|
||
* PID
|
||
* SN
|
||
* VID
|
||
* Timestamp
|
||
* Scenario
|
||
*/
|
||
|
||
interface HistoryItem {
|
||
pid: string
|
||
vid: string
|
||
sn: string
|
||
scenario: string
|
||
id: number
|
||
number: number
|
||
stationId: number
|
||
timestamp?: number
|
||
}
|
||
|
||
interface User {
|
||
userEmail: string
|
||
userName: string
|
||
}
|
||
|
||
interface DataDPELP {
|
||
line: number
|
||
pid: any
|
||
vid: any
|
||
sn: any
|
||
ios: string
|
||
mac: string
|
||
license: any
|
||
issues: string[]
|
||
summary: string
|
||
}
|
||
|
||
export default class LineConnection {
|
||
public client: net.Socket
|
||
public config: LineConfig
|
||
public readonly socketIO: any
|
||
private outputBuffer: string
|
||
private connecting: boolean
|
||
private waitingScenario: boolean
|
||
private outputInventory: string
|
||
private outputScenario: string
|
||
private bufferLog: LogStreamBuffer
|
||
public dataDPELP: DataDPELP | string
|
||
private listScenarios: number[]
|
||
public handleClearLine: () => void
|
||
private session: TestSession
|
||
public physicalTest: PhysicalPortTest
|
||
private outputPhysicalTest: string
|
||
private outputLoadIosLicense: string | boolean
|
||
private listDeviceIos: string[]
|
||
private debounceTimer: NodeJS.Timeout | null = null
|
||
private testingPortPoE: boolean
|
||
private outputTestingPortPoE: string
|
||
private debounceSendSummaryReport: NodeJS.Timeout | null = null
|
||
private isPingToServer: boolean
|
||
private outputPingToServer: string
|
||
private outputTestLog: string
|
||
|
||
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
|
||
this.config = config
|
||
this.socketIO = socketIO
|
||
this.client = new net.Socket()
|
||
this.outputBuffer = ''
|
||
this.connecting = false
|
||
this.waitingScenario = false
|
||
this.outputInventory = ''
|
||
this.outputScenario = ''
|
||
this.bufferLog = new LogStreamBuffer()
|
||
this.dataDPELP = {
|
||
line: this.config.lineNumber,
|
||
pid: '',
|
||
vid: '',
|
||
sn: '',
|
||
ios: '',
|
||
mac: '',
|
||
license: [],
|
||
issues: ['No data'],
|
||
summary: '',
|
||
}
|
||
this.listScenarios = []
|
||
this.session = new TestSession()
|
||
this.handleClearLine = handleClearLine
|
||
this.physicalTest = new PhysicalPortTest([])
|
||
this.outputPhysicalTest = ''
|
||
this.outputLoadIosLicense = ''
|
||
this.listDeviceIos = []
|
||
this.debounceTimer = null
|
||
this.debounceSendSummaryReport = null
|
||
this.testingPortPoE = false
|
||
this.outputTestingPortPoE = ''
|
||
this.isPingToServer = false
|
||
this.outputPingToServer = ''
|
||
this.outputTestLog = ''
|
||
}
|
||
/**
|
||
* Connect to line with socket
|
||
*/
|
||
connect(timeoutMs = 5000) {
|
||
return new Promise<void>((resolve, reject) => {
|
||
const { ip, port, lineNumber, id, stationId } = this.config
|
||
let resolvedOrRejected = false
|
||
// Set timeout
|
||
this.client.setTimeout(timeoutMs)
|
||
console.log(`🔌 Connecting to line ${lineNumber} (${ip}:${port})...`)
|
||
this.client.connect(port, ip, () => {
|
||
if (resolvedOrRejected) return
|
||
resolvedOrRejected = true
|
||
|
||
console.log(`[${Date.now()}] ✅ Connected to line ${lineNumber} (${ip}:${port})`)
|
||
this.connecting = true
|
||
setTimeout(() => {
|
||
this.config.status = 'connected'
|
||
// this.retryConnect = 0
|
||
this.connecting = false
|
||
this.socketIO.emit('line_connected', {
|
||
stationId,
|
||
lineId: id,
|
||
lineNumber,
|
||
status: 'connected',
|
||
})
|
||
this.config.listFeatureTested = []
|
||
this.config.isSkipPhysical = false
|
||
this.config.reasonSkipPhysical = ''
|
||
this.sendFeatureTested()
|
||
this.checkLog()
|
||
resolve()
|
||
}, 2000)
|
||
})
|
||
|
||
this.client.on('data', (data) => {
|
||
let message = this.connecting ? cleanData(data.toString()) : data.toString()
|
||
const lines = this.bufferLog.push(data)
|
||
lines.forEach(this.handleLogLine)
|
||
let rawData = ''
|
||
if (this.config.runningScenario) {
|
||
this.waitingScenario = true
|
||
this.outputBuffer += message
|
||
this.outputScenario += message
|
||
this.outputTestLog += message
|
||
if (!this.config.inventory)
|
||
this.outputInventory = this.outputInventory.slice(-3000) + message
|
||
}
|
||
if (this.outputLoadIosLicense) {
|
||
if (this.outputLoadIosLicense === true) this.outputLoadIosLicense = ''
|
||
this.outputLoadIosLicense += cleanData(data.toString())
|
||
}
|
||
if (this.config.runningPhysical) {
|
||
this.outputPhysicalTest += message
|
||
this.outputTestingPortPoE += message
|
||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||
|
||
if (this.testingPortPoE)
|
||
this.debounceTimer = setTimeout(() => {
|
||
this.flushLogBuffer()
|
||
}, 1000) // 1s debounce
|
||
}
|
||
if (this.isPingToServer) this.outputPingToServer += cleanData(data.toString())
|
||
|
||
if (data.toString().includes('More') || data.toString().includes('MORE'))
|
||
this.writeCommand(' ')
|
||
|
||
// let output = cleanData(message)
|
||
// console.log(`📨 [${this.config.port}] ${message}`)
|
||
// Handle netOutput with backspace support
|
||
for (const char of message) {
|
||
if (char === '\x7F' || char === '\x08') {
|
||
this.config.output = this.config.output.slice(0, -1)
|
||
// message = message.slice(0, -1)
|
||
} else {
|
||
rawData += char
|
||
}
|
||
}
|
||
this.config.output += cleanData(rawData)
|
||
this.config.output = this.config.output.slice(-15000)
|
||
if (!this.config.isReady && canInputCommand(message)) {
|
||
this.config.isReady = true
|
||
this.socketIO.emit('update_status_ready', {
|
||
stationId,
|
||
lineId: id,
|
||
isReady: true,
|
||
})
|
||
}
|
||
this.socketIO.emit('line_output', {
|
||
stationId,
|
||
lineId: id,
|
||
data: message,
|
||
ports: this.config.ports,
|
||
})
|
||
setTimeout(() => {
|
||
if (!this.config.inventory) {
|
||
this.getInventory()
|
||
}
|
||
}, 5000)
|
||
appendLog(
|
||
cleanData(message),
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
})
|
||
|
||
this.client.on('error', (err) => {
|
||
if (resolvedOrRejected) return
|
||
resolvedOrRejected = true
|
||
console.error(`❌ Error line ${lineNumber}:`, err.message)
|
||
this.config.output += '\r\n' + err.message + '\r\n'
|
||
this.socketIO.emit('line_error', {
|
||
stationId,
|
||
lineId: id,
|
||
error: '\r\n' + err.message + '\r\n',
|
||
})
|
||
this.endTesting()
|
||
resolve()
|
||
})
|
||
|
||
this.client.on('close', async () => {
|
||
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.config.isSkipPhysical = false
|
||
this.config.reasonSkipPhysical = ''
|
||
this.config.latestScenario = undefined
|
||
this.physicalTest = new PhysicalPortTest([])
|
||
this.config.isReady = false
|
||
// this.config.inventory = undefined
|
||
this.socketIO.emit('line_disconnected', {
|
||
stationId,
|
||
lineId: id,
|
||
lineNumber,
|
||
status: 'disconnected',
|
||
})
|
||
// if (this.retryConnect <= 5) {
|
||
// await this.sleep(5000)
|
||
// console.log(`Retry connect line [${this.config.lineNumber}] times`, this.retryConnect)
|
||
// this.retryConnect += 1
|
||
// await this.reconnect()
|
||
// } else {
|
||
// this.retryConnect = 0
|
||
// }
|
||
this.endTesting()
|
||
})
|
||
|
||
this.client.on('timeout', () => {
|
||
if (resolvedOrRejected) return
|
||
resolvedOrRejected = true
|
||
const message = '\r\nConnection timeout!!\r\n'
|
||
this.config.output += message
|
||
this.socketIO.emit('line_output', {
|
||
stationId,
|
||
lineId: id,
|
||
data: message,
|
||
})
|
||
appendLog(
|
||
cleanData(message),
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
console.log(`⏳ Connection timeout line ${lineNumber}`)
|
||
this.client.destroy()
|
||
resolve()
|
||
// reject(new Error('Connection timeout'))
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Waiting with millisecond
|
||
*/
|
||
private sleep(ms: number) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||
}
|
||
|
||
/**
|
||
* Write a command with socket.write
|
||
*/
|
||
async writeCommand(cmd: string | Buffer<ArrayBuffer>) {
|
||
if (this.client.destroyed) {
|
||
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||
this.disconnect()
|
||
// this.disconnect()
|
||
// await sleep(2000)
|
||
// await this.connect()
|
||
|
||
return
|
||
}
|
||
|
||
// console.log(
|
||
// `Write command "${cmd.toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}" to line ${this.config.lineNumber} of ${this.config.stationName}`
|
||
// )
|
||
|
||
this.client.write(cmd)
|
||
}
|
||
|
||
/**
|
||
* Disconnect socket with line
|
||
*/
|
||
async disconnect() {
|
||
try {
|
||
console.log('[DISCONNECT] Line', this.config.lineNumber)
|
||
// this.handleClearLine()
|
||
this.client.destroy()
|
||
this.config.status = 'disconnected'
|
||
this.socketIO.emit('line_disconnected', {
|
||
...this.config,
|
||
status: 'disconnected',
|
||
})
|
||
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
|
||
} catch (e) {
|
||
console.error('Error closing line:', e)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Run a scenario as DPELP, Breaking password, load ios,...
|
||
*/
|
||
async runScript(script: Scenario, userName: string) {
|
||
if (!this.client || this.client.destroyed) {
|
||
console.log('Not connected')
|
||
this.config.runningScenario = ''
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: '',
|
||
})
|
||
this.outputBuffer = ''
|
||
return
|
||
}
|
||
if (!this.config.isReady) {
|
||
console.log('Device is not ready')
|
||
return
|
||
}
|
||
if (this.config.runningScenario || this.config.runningPhysical) {
|
||
console.log('Script already running')
|
||
return
|
||
}
|
||
|
||
console.log(
|
||
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
|
||
)
|
||
this.config.runningScenario = script?.title
|
||
this.config.data = []
|
||
this.outputScenario = ''
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: script?.title,
|
||
})
|
||
if (script?.send_result || script?.sendResult) {
|
||
this.dataDPELP = ''
|
||
// this.config.inventory = ''
|
||
}
|
||
|
||
if (script?.isReboot) {
|
||
await sleep(10000)
|
||
for (let index = 0; index < 30; index++) {
|
||
await sleep(1000)
|
||
this.breakSpam()
|
||
}
|
||
}
|
||
|
||
const now = Date.now()
|
||
this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`
|
||
appendLog(
|
||
`\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`,
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
this.config.latestScenario = {
|
||
name: script?.title,
|
||
time: now,
|
||
}
|
||
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
|
||
let stepIndex = 0
|
||
|
||
// Create a timeout
|
||
let timeoutTimer: NodeJS.Timeout | null = null
|
||
const timeoutNumber = script.timeout ? Number(script.timeout) * 1000 : 300000
|
||
const onTimeout = () => {
|
||
this.config.runningScenario = ''
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: '',
|
||
})
|
||
this.outputBuffer = ''
|
||
this.outputScenario = ''
|
||
this.config.output += '\nTimeout run scenario\n'
|
||
this.dataDPELP = {
|
||
line: this.config.lineNumber,
|
||
pid: '',
|
||
vid: '',
|
||
sn: '',
|
||
ios: '',
|
||
mac: '',
|
||
license: [],
|
||
issues: ['No data'],
|
||
summary: '',
|
||
}
|
||
this.socketIO.emit('line_output', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data: '\nTimeout run scenario\n',
|
||
})
|
||
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
|
||
appendLog(
|
||
`\n---end-scenarios---${now}---${userName}---\n`,
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
// reject(new Error('Script timeout'))
|
||
}
|
||
const resetTimeout = () => {
|
||
// console.log('resetTimeout', timeoutNumber)
|
||
// this.outputBuffer = ''
|
||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||
timeoutTimer = setTimeout(onTimeout, timeoutNumber)
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
timeoutTimer = setTimeout(onTimeout, timeoutNumber)
|
||
|
||
const runStep = async (index: number) => {
|
||
if (index >= steps.length) {
|
||
if (this.waitingScenario) {
|
||
this.waitingScenario = false
|
||
setTimeout(() => {
|
||
runStep(index)
|
||
}, 5000)
|
||
return
|
||
} else if (timeoutTimer) clearTimeout(timeoutTimer)
|
||
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
|
||
this.outputBuffer = ''
|
||
this.config.runningScenario = ''
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: '',
|
||
})
|
||
|
||
const logScenarios = this.outputScenario
|
||
const data = textfsmResults(logScenarios, '')
|
||
let pid = this.config.inventory?.pid || ''
|
||
try {
|
||
for (const item of data) {
|
||
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
||
if (
|
||
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
|
||
) {
|
||
const listInventory = JSON.parse(item.textfsm)
|
||
const dataInventory = listInventory[0]
|
||
this.config.inventory = this.config.inventory
|
||
? { ...this.config.inventory, ...dataInventory, listInventory }
|
||
: { ...dataInventory, listInventory }
|
||
pid = dataInventory?.pid || ''
|
||
this.addHistory(this.config.stationId, this.config.id, {
|
||
id: this.config.id,
|
||
number: this.config.lineNumber,
|
||
stationId: this.config.stationId,
|
||
pid: dataInventory?.pid,
|
||
sn: dataInventory?.sn,
|
||
vid: dataInventory?.vid,
|
||
scenario: script?.title,
|
||
timestamp: Date.now(),
|
||
})
|
||
}
|
||
if (['show version', 'sh version', 'show ver', 'sh ver'].includes(item.command)) {
|
||
const dataVer = JSON.parse(item.textfsm)[0]
|
||
this.config.inventory = this.config.inventory
|
||
? { ...this.config.inventory, ...dataVer }
|
||
: dataVer
|
||
if (pid && (dataVer?.MEMORY || dataVer?.USB_FLASH)) {
|
||
await this.checkConfigRam(
|
||
dataVer?.MEMORY || '',
|
||
dataVer?.USB_FLASH || '',
|
||
pid,
|
||
cleanData(item.output)
|
||
)
|
||
}
|
||
}
|
||
if (
|
||
item.command?.trim()?.includes('show env') ||
|
||
item.command?.trim()?.includes('sh env')
|
||
) {
|
||
const dataEnv = await this.detectShowEnvWithAI(item.output)
|
||
item.dataAI = dataEnv
|
||
}
|
||
item.textfsm = JSON.parse(item.textfsm)
|
||
}
|
||
}
|
||
const scenario = await detectScenarioByModel(pid, this.listScenarios)
|
||
console.log(pid, scenario?.title, this.listScenarios)
|
||
if (
|
||
scenario &&
|
||
scenario.id !== script.id &&
|
||
scenario.title.includes('DPELP') &&
|
||
script.title.includes('DPELP')
|
||
) {
|
||
this.listScenarios.push(scenario.id)
|
||
// this.outputScenario = ''
|
||
this.runScript(scenario, userName)
|
||
// this.socketIO.emit('confirm_scenario', {
|
||
// scenario: scenario,
|
||
// id: this.config.id,
|
||
// })
|
||
resolve(true)
|
||
return
|
||
}
|
||
if (script?.send_result || script?.sendResult) {
|
||
const detectLog = await this.detectLogWithAI(logScenarios)
|
||
const result = mapToLineFormat({
|
||
lineNumber: this.config.lineNumber,
|
||
inventory: this.config.inventory,
|
||
latestScenario: {
|
||
detectAI: detectLog,
|
||
},
|
||
data,
|
||
})
|
||
// if (script?.send_result || script?.sendResult) {
|
||
this.dataDPELP = result
|
||
console.log(
|
||
`DPELP DATA line ${this.config.lineNumber} of ${this.config.stationName}:`,
|
||
this.dataDPELP
|
||
)
|
||
this.config.listFeatureTested = [
|
||
...new Set([...this.config.listFeatureTested, 'DPELP']),
|
||
]
|
||
// if (!this.config.listFeatureTested.includes('PHYSICAL')) this.runPhysicalTest()
|
||
this.sendFeatureTested()
|
||
|
||
// Set timeout send report
|
||
// this.setTimeoutSendSummaryReport(
|
||
// !this.config.listFeatureTested.includes('PHYSICAL') ? 600000 : 30000
|
||
// )
|
||
|
||
// }
|
||
if (this.config.latestScenario)
|
||
this.config.latestScenario = {
|
||
...this.config.latestScenario,
|
||
detectAI: { issue: detectLog, summary: '', status: [] },
|
||
}
|
||
// if (result.sn) {
|
||
// this.updateNote(result.sn, result)
|
||
// }
|
||
}
|
||
this.config.data = data
|
||
this.socketIO.emit('data_textfsm', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data,
|
||
inventory: this.config.inventory || null,
|
||
latestScenario: this.config.latestScenario || null,
|
||
})
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
appendLog(
|
||
`\n---end-scenarios---${now}---${userName}---\n`,
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
this.listScenarios = []
|
||
resolve(true)
|
||
return
|
||
} else resetTimeout()
|
||
|
||
const step = steps[index]
|
||
let repeatCount = Number(step.repeat) || 1
|
||
const delay = step?.delay ? Number(step?.delay) * 1000 : 1000
|
||
const sendCommand = async () => {
|
||
// if (repeatCount <= 0) {
|
||
// // Done → next step
|
||
// stepIndex++
|
||
// return runStep(stepIndex)
|
||
// }
|
||
|
||
if (typeof step.send !== 'undefined') {
|
||
console.log(Date.now() - now, (step?.send ?? '[ENTER]').toString())
|
||
this.outputScenario += `\n---send-command---"${(step?.send ?? '[ENTER]').toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"---${now}---\n`
|
||
appendLog(
|
||
`\n---send-command---"${(step?.send ?? '[ENTER]').toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"---${now}---\n`,
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
this.writeCommand((step?.send || '') + '\r\n')
|
||
}
|
||
|
||
repeatCount--
|
||
if (repeatCount <= 0) {
|
||
// Done → next step
|
||
stepIndex++
|
||
return runStep(stepIndex)
|
||
} else setTimeout(() => sendCommand(), delay)
|
||
}
|
||
|
||
// Nếu expect rỗng → gửi ngay
|
||
if (!step?.expect || step?.expect.trim() === '') {
|
||
setTimeout(() => sendCommand(), delay)
|
||
return
|
||
}
|
||
|
||
// while (this.outputBuffer) {
|
||
// await sleep(200)
|
||
// if (this.outputBuffer.includes(step.expect)) {
|
||
// this.outputBuffer = ''
|
||
// setTimeout(() => sendCommand(), delay)
|
||
// }
|
||
// }
|
||
|
||
const matched = await this.waitForExpect(
|
||
step.expect.trim(),
|
||
script?.timeout ? Number(script?.timeout) * 1000 : 60000
|
||
)
|
||
if (matched) setTimeout(() => sendCommand(), delay)
|
||
}
|
||
|
||
runStep(stepIndex)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Reconnect socket with line
|
||
*/
|
||
public async reconnect(): Promise<boolean> {
|
||
try {
|
||
this.disconnect()
|
||
this.client = new net.Socket()
|
||
await this.sleep(1000)
|
||
await this.connect()
|
||
return true
|
||
} catch (err: any) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* User open CLI from front-end
|
||
*/
|
||
userOpenCLI(user: User) {
|
||
this.config.openCLI = true
|
||
this.config.userEmailOpenCLI = user.userEmail
|
||
this.config.userOpenCLI = user.userName
|
||
this.socketIO.emit('user_open_cli', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
userEmailOpenCLI: user.userEmail,
|
||
userOpenCLI: user.userName,
|
||
})
|
||
appendLog(
|
||
`\n-------${user.userName}-------\n`,
|
||
this.config.stationId,
|
||
this.config.stationName,
|
||
this.config.stationIp,
|
||
this.config.lineNumber
|
||
)
|
||
}
|
||
|
||
/**
|
||
* User close CLI from front-end
|
||
*/
|
||
userCloseCLI() {
|
||
this.config.openCLI = false
|
||
this.config.userEmailOpenCLI = ''
|
||
this.config.userOpenCLI = ''
|
||
this.socketIO.emit('user_close_cli', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
userEmailOpenCLI: '',
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Clear output buffer
|
||
*/
|
||
clearCLI() {
|
||
this.config.output = ''
|
||
this.socketIO.emit('user_clear_terminal', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
})
|
||
setTimeout(() => this.writeCommand('\r\n'), 100)
|
||
}
|
||
|
||
/**
|
||
* Waiting for a expect with until catch it from output
|
||
*/
|
||
waitForExpect = async (expect: string, timeout = 60000) => {
|
||
const start = Date.now()
|
||
// console.log('[EXPECT]', expect, timeout)
|
||
while (Date.now() - start < timeout) {
|
||
if (this.outputBuffer.includes(expect)) {
|
||
this.outputBuffer = ''
|
||
return true
|
||
}
|
||
await sleep(200)
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Detect inventory data from output
|
||
*/
|
||
getInventory = () => {
|
||
const data = textfsmResults(this.outputInventory, 'show inventory')
|
||
try {
|
||
data.forEach((item) => {
|
||
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
||
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
|
||
const listInventory = JSON.parse(item.textfsm)
|
||
const dataInventory = listInventory[0]
|
||
this.config.inventory = this.config.inventory
|
||
? { ...this.config.inventory, ...dataInventory, listInventory }
|
||
: dataInventory
|
||
}
|
||
item.textfsm = JSON.parse(item.textfsm)
|
||
}
|
||
})
|
||
if (this.config.inventory) {
|
||
this.config.data = data
|
||
this.socketIO.emit('data_textfsm', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data,
|
||
inventory: this.config.inventory || null,
|
||
latestScenario: this.config.latestScenario || null,
|
||
})
|
||
this.outputInventory = ''
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gửi nhiều ký tự ESC để vào ROMMON
|
||
*/
|
||
breakSpam() {
|
||
console.log('SPAM Break to line:', this.config.lineNumber)
|
||
let count = 0
|
||
const escInterval = setInterval(() => {
|
||
if (count >= 10) {
|
||
clearInterval(escInterval)
|
||
return
|
||
}
|
||
this.client.write(Buffer.from([0xff, 0xf3])) // Ctrl + Break
|
||
count++
|
||
}, 1)
|
||
}
|
||
|
||
/**
|
||
* Set Baud of line
|
||
*/
|
||
async setBaud(baud: number) {
|
||
this.writeCommand('enable\r\n')
|
||
await sleep(500)
|
||
this.writeCommand('configure terminal\r\n')
|
||
await sleep(500)
|
||
this.writeCommand('line console 0\r\n')
|
||
await sleep(500)
|
||
this.writeCommand(`speed ${baud.toString()}\r\n`)
|
||
await sleep(500)
|
||
this.writeCommand('end\r\n')
|
||
await sleep(500)
|
||
this.writeCommand('write memory\r\n')
|
||
this.writeCommand('\r\n')
|
||
}
|
||
|
||
/**
|
||
* Get content's log of line with date
|
||
*/
|
||
async getLog(date: string) {
|
||
const logDir = path.join('storage', 'system_logs')
|
||
const logFile = path
|
||
.join(
|
||
logDir,
|
||
`${date}-AUTO-Session.${this.config.stationName}-${this.config.stationId}-${this.config.stationIp}-${this.config.lineNumber}.log`
|
||
)
|
||
.replaceAll(' ', '_')
|
||
|
||
if (!fs.existsSync(logDir) || !fs.existsSync(logFile)) {
|
||
return ''
|
||
}
|
||
|
||
return await fs.promises.readFile(logFile, 'utf8')
|
||
}
|
||
|
||
/**
|
||
* Detect log by call api gpt, return summary and issues
|
||
*/
|
||
async detectLogWithAI(log: string) {
|
||
try {
|
||
// Get prompt from database
|
||
const promptRecord = await PromptAi.findBy('type', 'dpelp')
|
||
if (!promptRecord) {
|
||
console.log('[ERROR] Prompt DPELP not found in database')
|
||
return ''
|
||
}
|
||
|
||
const payload = {
|
||
model: 'gpt-4o-mini',
|
||
max_tokens: 1000,
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: `${promptRecord.content}
|
||
|
||
Return ONLY a valid JSON array of strings.
|
||
Here is the log:
|
||
${log}`,
|
||
},
|
||
],
|
||
}
|
||
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
|
||
const remoteResp = await axios.post(
|
||
remoteUrl + '/api/transferPostData',
|
||
{
|
||
urlAPI: '/api/open-ai-sfp/model-image-info',
|
||
data: payload,
|
||
},
|
||
{
|
||
headers: {
|
||
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
|
||
},
|
||
}
|
||
)
|
||
return remoteResp.data?.Status === 'OK' ? remoteResp.data?.data : ''
|
||
} catch (error: any) {
|
||
console.log('[ERROR] Detect log from AI', error)
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* Add cache to list history devices on this line
|
||
*/
|
||
async addHistory(stationId: number, lineId: number, item: HistoryItem) {
|
||
if (!item.pid || !item.sn) return
|
||
const key = `station:${stationId}:line:${lineId}:history`
|
||
const now = Date.now()
|
||
|
||
const newItem = JSON.stringify({
|
||
...item,
|
||
timestamp: now,
|
||
})
|
||
|
||
// Lấy phần tử cuối
|
||
const lastItems = await redis.zrevrange(key, 0, 0)
|
||
if (lastItems.length > 0) {
|
||
const last = JSON.parse(lastItems[0])
|
||
|
||
if (last.pid === item.pid && last.sn === item.sn) {
|
||
return false // không thay đổi
|
||
}
|
||
}
|
||
|
||
const line = await Line.find(lineId)
|
||
if (line) {
|
||
const listHistory = line.history ? JSON.parse(line.history) : []
|
||
listHistory.unshift(newItem)
|
||
line.history = JSON.stringify(listHistory)
|
||
await line.save()
|
||
}
|
||
|
||
// Add vào ZSET
|
||
await redis.zadd(key, now, newItem)
|
||
|
||
// Tự động xóa item > 96h
|
||
const expireTime = now - 96 * 60 * 60 * 1000
|
||
await redis.zremrangebyscore(key, 0, expireTime)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* Get list history devices
|
||
*/
|
||
async getHistory(stationId: number, lineId: number) {
|
||
const key = `station:${stationId}:line:${lineId}:history`
|
||
const items = await redis.zrange(key, 0, -1)
|
||
return items.map((i) => JSON.parse(i))
|
||
}
|
||
|
||
/**
|
||
* Handle raw log to regex error
|
||
*/
|
||
handleLogLine = (line: string) => {
|
||
try {
|
||
const parsed = classifyLog(line)
|
||
this.session.applyParsedLog(parsed)
|
||
} catch (error) {
|
||
console.log('handleLogLine', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check raw log was regex each 5 minutes, if has error will send email report
|
||
*/
|
||
checkLog() {
|
||
const interval = setInterval(async () => {
|
||
try {
|
||
if (this.config.status !== 'connected') {
|
||
clearInterval(interval)
|
||
this.session.clear()
|
||
this.bufferLog.clear()
|
||
return
|
||
}
|
||
const result = this.session.finalize()
|
||
|
||
if (result.errors.length === 0) {
|
||
this.session.clear()
|
||
this.bufferLog.clear()
|
||
return
|
||
}
|
||
|
||
// const detectLog = await this.detectLogWithAI(this.bufferLog.allBuffer)
|
||
// console.log(detectLog)
|
||
const tableHTML = this.buildEmailContent(result)
|
||
await sendMessageToMail(
|
||
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue ${result?.errors?.some((e) => e.category === 'SPECIAL_KEYWORD') ? '+ Special keywords' : ''}`,
|
||
tableHTML +
|
||
`${`
|
||
<hr />
|
||
<p>Logs:</p>
|
||
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
|
||
${this.bufferLog.allBuffer}</span></div>`}`
|
||
)
|
||
this.session.clear()
|
||
this.bufferLog.clear()
|
||
} catch (err: any) {
|
||
console.error('Error checking log:', err)
|
||
}
|
||
}, 300000)
|
||
}
|
||
|
||
/**
|
||
* Render table to view error
|
||
*/
|
||
renderErrorTable(rows: ErrorRow[]): string {
|
||
if (!rows.length) {
|
||
return `<p style="color: green;">No errors detected</p>`
|
||
}
|
||
|
||
const header = `
|
||
<tr>
|
||
<th style="padding:6px; text-align:center;">Level</th>
|
||
<th style="padding:6px; text-align:center;">Rule</th>
|
||
<th style="padding:6px; text-align:center;">Message</th>
|
||
<th style="padding:6px; width:1000px;">Log Evidence</th>
|
||
</tr>
|
||
`
|
||
|
||
const body = rows
|
||
.map(
|
||
(r) => `
|
||
<tr>
|
||
<td style="padding:6px; text-align:center;">
|
||
<span style="color:${r.level === 'FAIL' ? 'red' : 'orange'};">${r.level}</span>
|
||
</td>
|
||
<td style="padding:6px; text-align:center;">${r.rule}</td>
|
||
<td style="padding:6px; text-align:center;">${r.message}</td>
|
||
<td style="padding:6px; font-family:monospace;">${escapeHtml(r.log.trim())
|
||
.split('*')
|
||
.filter((el) => el)
|
||
.join('<br/>*')}</td>
|
||
</tr>
|
||
`
|
||
)
|
||
.join('')
|
||
|
||
return `
|
||
<table border="1" cellpadding="6" style="border-collapse: collapse; width:100%;">
|
||
${header}
|
||
${body}
|
||
</table>
|
||
`
|
||
}
|
||
|
||
/**
|
||
* Return a body email
|
||
*/
|
||
buildEmailContent(result: TestResult): string {
|
||
const rows = mapErrorsToRows(result.errors)
|
||
const table = this.renderErrorTable(rows)
|
||
|
||
return `
|
||
<h3>Cisco Device Log Result</h3>
|
||
<p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p>
|
||
<p><b>Summary:</b> ${result.summary}</p>
|
||
<hr />
|
||
${table}
|
||
<br />
|
||
`
|
||
}
|
||
|
||
/**
|
||
* Update note of SN to ERP after run DPELP
|
||
*/
|
||
async updateNote(sn: string, data: DataDPELP) {
|
||
const portPhysical = Array.from(this.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 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 licenses = Array.isArray(data.license)
|
||
? [...new Set(data.license)]
|
||
: data.license
|
||
? [data.license]
|
||
: []
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm')
|
||
const note = `-------[ATC]-[${dataFormat}]-------
|
||
*****[DPELP]*****
|
||
License: ${licenses.join(', ')}
|
||
Detected by AI:
|
||
${data.issues?.length ? `- ` + data.issues.join(`\n`) : ''}
|
||
*****[Physical]*****
|
||
Total Ports: ${portPhysical?.length}
|
||
Ports Tested (Link UP): ${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)
|
||
Ports Missing/Down: ${missing.length}
|
||
${this.config.reasonSkipPhysical ? `***User skip test ports:\n- ${this.config.reasonSkipPhysical}` : ''}
|
||
\n`
|
||
await updateNoteToERP(sn, note)
|
||
}
|
||
|
||
/**
|
||
* Update note of SN to ERP from user input
|
||
*/
|
||
async updateNoteFromUser(sn: string, note: string, licenses: string[]) {
|
||
const portPhysical = Array.from(this.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 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 timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm')
|
||
const data = `-------[ATC]-[${dataFormat}]-------
|
||
*****[DPELP]*****
|
||
License: ${licenses.join(', ')}
|
||
Issues:
|
||
${note}
|
||
*****[Physical]*****
|
||
Total Ports: ${portPhysical?.length}
|
||
Ports Tested (Link UP): ${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)
|
||
Ports Missing/Down: ${missing.length}\n\n`
|
||
const issueList = note
|
||
.split('\n')
|
||
.map((line) => (line[0] === '-' ? line.substring(1).trim() : line.trim()))
|
||
|
||
const detectAI = this.config?.latestScenario?.detectAI
|
||
? { ...this.config.latestScenario.detectAI, issue: issueList }
|
||
: { issue: issueList, summary: '', status: [] }
|
||
|
||
if (this.config.latestScenario) {
|
||
this.config.latestScenario = { ...this.config.latestScenario, detectAI }
|
||
}
|
||
|
||
await updateNoteToERP(sn, data)
|
||
}
|
||
|
||
/**
|
||
* Starting physical test (PoE ports testing)
|
||
*/
|
||
async runPhysicalTest(userName?: string) {
|
||
if (this.config.runningPhysical) {
|
||
console.log('Running physical test')
|
||
return
|
||
}
|
||
this.setTimeoutSendSummaryReport(600000)
|
||
this.config.runningPhysical = true
|
||
this.config.runningScenario = 'Physical Test'
|
||
this.config.isSkipPhysical = false
|
||
this.config.reasonSkipPhysical = ''
|
||
this.testingPortPoE = true
|
||
this.outputTestingPortPoE = ''
|
||
const listPorts = await this.getPorts()
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: 'Physical Test',
|
||
physical: true,
|
||
ports: listPorts,
|
||
})
|
||
if (listPorts.length === 0) {
|
||
this.config.listFeatureTested = [...new Set([...this.config.listFeatureTested, 'PHYSICAL'])]
|
||
this.config.isSkipPhysical = true
|
||
this.config.reasonSkipPhysical = ''
|
||
this.sendFeatureTested()
|
||
console.log('End physical test')
|
||
this.endTesting()
|
||
return
|
||
}
|
||
|
||
this.physicalTest.start(
|
||
listPorts.map((el) => el),
|
||
this.config.inventory
|
||
)
|
||
const interval = setInterval(async () => {
|
||
if (!this.config.runningPhysical || this.config.status !== 'connected') {
|
||
clearInterval(interval)
|
||
} else if (this.physicalTest.done) {
|
||
clearInterval(interval)
|
||
this.sendReportPhysicalTest()
|
||
this.endTesting()
|
||
} else {
|
||
this.checkingPhysicalPort()
|
||
}
|
||
}, 15000)
|
||
}
|
||
|
||
async checkingPhysicalPort() {
|
||
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,
|
||
})
|
||
if (ports.length === this.config.ports.length) {
|
||
this.sendReportPhysicalTest()
|
||
this.endTesting()
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log('checkingPhysicalPort', error)
|
||
}
|
||
}
|
||
|
||
async flushLogBuffer() {
|
||
try {
|
||
const lines = this.outputTestingPortPoE.split(/\r?\n/)
|
||
|
||
// giữ lại dòng cuối nếu chưa kết thúc hoàn chỉnh
|
||
this.outputTestingPortPoE = lines.pop() || ''
|
||
|
||
const completeLines = lines.join('\n')
|
||
|
||
if (completeLines.trim()) {
|
||
const ports = this.physicalTest.handleLog(completeLines)
|
||
if (ports?.length)
|
||
this.socketIO.emit('test_port_physical', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data: ports,
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.log('flushLogBuffer', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* End all testing
|
||
*/
|
||
endTesting() {
|
||
this.physicalTest.done = true
|
||
// this.physicalTest.resetTestedPorts()
|
||
this.config.runningPhysical = false
|
||
this.config.runningScenario = ''
|
||
this.testingPortPoE = false
|
||
this.outputTestingPortPoE = ''
|
||
this.outputBuffer = ''
|
||
this.outputScenario = ''
|
||
this.outputPhysicalTest = ''
|
||
this.config.ports = []
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: '',
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Get list PoE ports
|
||
*/
|
||
async getPorts(): Promise<string[]> {
|
||
this.writeCommand(' terminal length 0\r\n')
|
||
this.writeCommand(' show power inline\r\n')
|
||
this.writeCommand(' \r\n')
|
||
await this.sleep(3000)
|
||
this.writeCommand(' show interfaces status\r\n')
|
||
this.writeCommand(' \r\n')
|
||
await this.sleep(6000)
|
||
const statusOutput = this.outputPhysicalTest
|
||
this.outputPhysicalTest = ''
|
||
const lines = statusOutput.split('\n')
|
||
const ports = []
|
||
for (const line of lines) {
|
||
// 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]
|
||
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"
|
||
// 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)')
|
||
}
|
||
}
|
||
this.config.ports = [...new Set(ports)]
|
||
return [...new Set(ports)]
|
||
}
|
||
|
||
/**
|
||
* Send report after done physical test
|
||
*/
|
||
async sendReportPhysicalTest(reason?: string) {
|
||
this.config.listFeatureTested = [...new Set([...this.config.listFeatureTested, 'PHYSICAL'])]
|
||
if (typeof reason === 'string' && reason.trim().length > 0) {
|
||
this.config.isSkipPhysical = true
|
||
this.config.reasonSkipPhysical = reason
|
||
}
|
||
this.sendFeatureTested()
|
||
// Set timeout send report
|
||
if (
|
||
this.config.listFeatureTested?.includes('PHYSICAL') &&
|
||
this.config.listFeatureTested?.includes('DPELP')
|
||
)
|
||
this.setTimeoutSendSummaryReport(5000)
|
||
const formReport = this.physicalTest.getFormReport(this.config.inventory)
|
||
const reasonSkipPhysical =
|
||
typeof reason === 'string' && reason.trim().length > 0
|
||
? `<b style="color: #ff0000;">User Skip Test Port</b><br/>
|
||
────────────────────────────────<br/>
|
||
${reason}`
|
||
: ''
|
||
await sendMessageToMail(
|
||
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Physical Ports Test`,
|
||
formReport + reasonSkipPhysical
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Handle load ios for router
|
||
*/
|
||
async loadIosRouter(nameIos: string, userName: string) {
|
||
const station = await Station.find(this.config.stationId)
|
||
if (!station) return
|
||
this.outputLoadIosLicense = true
|
||
const network = station?.gateway || '172.25.1.1'
|
||
const tftpIp = station?.tftp_ip || '172.16.7.69'
|
||
const [a, b] = network.split('.').map(Number)
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const body = buildBody(
|
||
'ROUTER_IOS',
|
||
tftpIp,
|
||
nameIos,
|
||
`${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`,
|
||
`${station?.gateway ? station?.gateway : '0.0.0.0'}`,
|
||
this.listDeviceIos
|
||
)
|
||
|
||
const script = {
|
||
id: 0,
|
||
isReboot: true,
|
||
sendResult: false,
|
||
send_result: false,
|
||
title: 'Load IOS Router',
|
||
timeout: 1000,
|
||
body: JSON.stringify(body),
|
||
}
|
||
|
||
await sleep(5000)
|
||
await this.runScript(script as any, userName)
|
||
await this.sendEmailLoadIos(nameIos, startTime)
|
||
}
|
||
|
||
/**
|
||
* Handle load ios for switch
|
||
*/
|
||
async loadIosSwitch(nameIos: string, userName: string) {
|
||
const station = await Station.find(this.config.stationId)
|
||
if (!station) return
|
||
this.outputLoadIosLicense = true
|
||
const network = station?.gateway || '172.25.1.1'
|
||
const tftpIp = station?.tftp_ip || '172.16.7.69'
|
||
const [a, b] = network.split('.').map(Number)
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
|
||
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
|
||
await this.configAddressGateway(address, gateway, 'vlan 1')
|
||
const pingSuccess = await this.pingToServer(tftpIp)
|
||
if (!pingSuccess) return
|
||
await this.backupIos(nameIos)
|
||
|
||
const body = buildBody('SWITCH_IOS', tftpIp, nameIos, address, gateway, this.listDeviceIos)
|
||
|
||
const script = {
|
||
id: 0,
|
||
isReboot: false,
|
||
sendResult: false,
|
||
send_result: false,
|
||
title: 'Load IOS Switch',
|
||
timeout: 1000,
|
||
body: JSON.stringify(body),
|
||
}
|
||
|
||
await this.runScript(script as any, userName)
|
||
await this.sendEmailLoadIos(nameIos, startTime)
|
||
}
|
||
|
||
/**
|
||
* Send mail report after load ios
|
||
*/
|
||
async sendEmailLoadIos(nameIos: string, startTime: string) {
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const body = `
|
||
Load IOS Report<br/>
|
||
────────────────────────────────<br/>
|
||
Station : <b>${this.config.stationName}</b><br/>
|
||
Line : <b>${this.config.lineNumber}</b><br/>
|
||
IOS : <b>${nameIos}</b> <br/>
|
||
Started At : ${startTime}<br/>
|
||
Finished At : ${dataFormat}<br/>
|
||
<br/>
|
||
`.trim()
|
||
|
||
await sendMessageToMail(
|
||
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load IOS Report`,
|
||
body +
|
||
`${`
|
||
<hr />
|
||
<p>Logs:</p>
|
||
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
|
||
${this.outputLoadIosLicense}</span></div>`}`
|
||
)
|
||
this.outputLoadIosLicense = ''
|
||
}
|
||
|
||
/**
|
||
* Send mail report after load license
|
||
*/
|
||
async sendEmailLoadLicense(nameLicense: string, startTime: string) {
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const report = parseLicenseReport(
|
||
typeof this.outputLoadIosLicense === 'string' ? this.outputLoadIosLicense : ''
|
||
)
|
||
|
||
const body = `
|
||
Load License Report<br/>
|
||
────────────────────────────────<br/>
|
||
Station : <b>${this.config.stationName}</b><br/>
|
||
Line : <b>${this.config.lineNumber}</b><br/>
|
||
License : <b>${nameLicense}</b> <br/>
|
||
Started At : ${startTime}<br/>
|
||
Finished At : ${dataFormat}<br/>
|
||
────────────────────────────────<br/>
|
||
Summary licenses: <b>${report.summary.join(', ')}</b><br/>
|
||
Successful: <b>${report.imported.join(', ')}</b><br/>
|
||
Exist: <b>${report.exist.join(', ')}</b><br/>
|
||
Failed: <b>${report.failed.join(', ')}</b><br/>
|
||
<br/>
|
||
`.trim()
|
||
|
||
await sendMessageToMail(
|
||
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load License Report`,
|
||
body +
|
||
`${`
|
||
<hr />
|
||
<p>Logs:</p>
|
||
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
|
||
${this.outputLoadIosLicense}</span></div>`}`
|
||
)
|
||
this.outputLoadIosLicense = ''
|
||
}
|
||
|
||
/**
|
||
* Check list ios exist on flash
|
||
*/
|
||
async checkDeviceFlash() {
|
||
this.writeCommand(' enable\r\n')
|
||
this.writeCommand('show flash:\r\n')
|
||
await sleep(2000)
|
||
|
||
const output = this.outputBuffer
|
||
const ios: string[] = []
|
||
let match
|
||
|
||
const SWITCH_BIN_REGEX = /^\s*\d+\s+-rwx\s+\d+\s+.*?\s+([^\s]+\.bin)\s*$/gim
|
||
const ROUTER_BIN_REGEX = /^\s*\d+\s+(\d+)\s+.*?\s+([^\s]+\.bin)\s*$/gim
|
||
|
||
// 🔍 Detect device type
|
||
const isSwitch = output.includes('rwx')
|
||
const regex = isSwitch ? SWITCH_BIN_REGEX : ROUTER_BIN_REGEX
|
||
|
||
// reset regex state
|
||
regex.lastIndex = 0
|
||
|
||
while ((match = regex.exec(output)) !== null) {
|
||
ios.push(isSwitch ? match[1] : match[2])
|
||
}
|
||
|
||
return ios
|
||
}
|
||
|
||
/**
|
||
* Delete File on Flash
|
||
*/
|
||
async deleteFileOnFlash(fileName: string) {
|
||
await this.writeCommand(`delete flash:${fileName}\r\n`)
|
||
await this.writeCommand(`\r\n`)
|
||
await this.writeCommand(`\r\n`)
|
||
await sleep(3000)
|
||
}
|
||
|
||
/**
|
||
* Upload file from flash to TFTP server
|
||
*/
|
||
async uploadFileToServerTFTP(fileName: string, server: string) {
|
||
this.config.runningScenario = 'Upload file'
|
||
await this.writeCommand(`copy flash: tftp:\r\n`)
|
||
await this.writeCommand(`${fileName}\r\n`)
|
||
await this.writeCommand(`${server}\r\n`)
|
||
await this.writeCommand(`i/${fileName}\r\n`)
|
||
this.outputBuffer = ''
|
||
await sleep(5000)
|
||
while (true) {
|
||
if (this.outputBuffer.includes('#')) {
|
||
this.outputBuffer = ''
|
||
this.config.runningScenario = ''
|
||
return true
|
||
}
|
||
await sleep(5000)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get list ios from TFTP server
|
||
*/
|
||
async getListIos() {
|
||
try {
|
||
const controller = new IosLicenseController()
|
||
const listIos = await controller.getIos()
|
||
return listIos
|
||
} catch (error) {
|
||
console.log('Error get ios', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get current boot ios of device
|
||
*/
|
||
async getCurrentBootIos() {
|
||
this.writeCommand('show version | include System image\r\n')
|
||
await sleep(2000)
|
||
|
||
const match = this.outputBuffer.match(/"flash:(.+?)"/i)
|
||
this.outputBuffer = ''
|
||
|
||
return match ? match[1] : null
|
||
}
|
||
|
||
/**
|
||
* Backup ios to TFTP, after that delete it on flash for free space
|
||
*/
|
||
async backupIos(nameIos: string) {
|
||
const station = await Station.find(this.config.stationId)
|
||
if (!station) return
|
||
const server = station?.tftp_ip || '172.16.7.69'
|
||
// const currentBootIos = await this.getCurrentBootIos()
|
||
this.config.runningScenario = 'Backup IOS'
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: 'Backup IOS',
|
||
})
|
||
await sleep(1000)
|
||
const listIos = await this.getListIos()
|
||
const dataDevice = await this.checkDeviceFlash()
|
||
this.listDeviceIos = [...dataDevice]
|
||
console.log('Data Device Flash', dataDevice)
|
||
if (dataDevice && Array.isArray(dataDevice)) {
|
||
for (const ios of dataDevice) {
|
||
// if (ios === nameIos) {
|
||
// console.log(`SKIP active IOS: ${ios}`)
|
||
// continue
|
||
// }
|
||
if (listIos?.map((value) => value.name)?.includes(ios)) {
|
||
console.log(`Already backed up: ${ios}`)
|
||
if (ios !== nameIos) await this.deleteFileOnFlash(ios)
|
||
} else {
|
||
const ok = await this.uploadFileToServerTFTP(ios, server)
|
||
if (ok && ios !== nameIos) await this.deleteFileOnFlash(ios)
|
||
}
|
||
}
|
||
}
|
||
this.outputBuffer = ''
|
||
this.config.runningScenario = ''
|
||
await sleep(1000)
|
||
}
|
||
|
||
/**
|
||
* Handle load License for switch
|
||
* Assumes traditional licensing (PAK/file-based) via TFTP
|
||
*/
|
||
async loadLicenseSwitch(licenseFileName: string, userName: string, portName: string) {
|
||
const station = await Station.find(this.config.stationId)
|
||
if (!station) return
|
||
this.outputLoadIosLicense = true
|
||
// Setup network variables (giống hệt logic load IOS để đảm bảo thông mạng)
|
||
const network = station?.gateway || '172.25.1.1'
|
||
const tftpIp = station?.tftp_ip || '172.16.7.69'
|
||
const [a, b] = network.split('.').map(Number)
|
||
|
||
// Setup time/logging
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
|
||
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
|
||
await this.configAddressGateway(address, gateway, portName)
|
||
const pingSuccess = await this.pingToServer(tftpIp)
|
||
if (!pingSuccess) return
|
||
|
||
const body = buildBody(
|
||
'SWITCH_LICENSE',
|
||
tftpIp,
|
||
licenseFileName,
|
||
address,
|
||
gateway,
|
||
this.listDeviceIos,
|
||
portName
|
||
)
|
||
|
||
const script = {
|
||
id: 0,
|
||
isReboot: false,
|
||
sendResult: false,
|
||
send_result: false,
|
||
title: 'Load License Switch',
|
||
timeout: 1000,
|
||
body: JSON.stringify(body),
|
||
}
|
||
|
||
await this.runScript(script as any, userName)
|
||
await this.sendEmailLoadLicense(licenseFileName, startTime) // Nếu bạn có hàm gửi mail báo cáo
|
||
}
|
||
|
||
/**
|
||
* Handle load License for Router
|
||
*/
|
||
async loadLicenseRouter(licenseFileName: string, userName: string, portName: string) {
|
||
const station = await Station.find(this.config.stationId)
|
||
if (!station) return
|
||
this.outputLoadIosLicense = true
|
||
const network = station?.gateway || '172.25.1.1'
|
||
const tftpIp = station?.tftp_ip || '172.16.7.69'
|
||
const [a, b] = network.split('.').map(Number)
|
||
|
||
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||
const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
|
||
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
|
||
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
|
||
await this.configAddressGateway(address, gateway, portName, true)
|
||
const pingSuccess = await this.pingToServer(tftpIp)
|
||
if (!pingSuccess) return
|
||
|
||
const body = buildBody(
|
||
'ROUTER_LICENSE',
|
||
tftpIp,
|
||
licenseFileName,
|
||
address,
|
||
gateway,
|
||
this.listDeviceIos,
|
||
portName
|
||
)
|
||
|
||
const script = {
|
||
id: 0,
|
||
isReboot: false,
|
||
sendResult: false,
|
||
send_result: false,
|
||
title: 'Load License Router',
|
||
timeout: 1000,
|
||
body: JSON.stringify(body),
|
||
}
|
||
|
||
await this.runScript(script as any, userName)
|
||
await this.sendEmailLoadLicense(licenseFileName, startTime)
|
||
}
|
||
|
||
/**
|
||
* Detect log by call api gpt, return string[]
|
||
*/
|
||
async detectShowEnvWithAI(log: string) {
|
||
try {
|
||
// Get prompt from database
|
||
const promptRecord = await PromptAi.findBy('type', 'env')
|
||
if (!promptRecord) {
|
||
console.log('[ERROR] Prompt ENV not found in database')
|
||
return ''
|
||
}
|
||
|
||
const payload = {
|
||
model: 'gpt-4o-mini',
|
||
max_tokens: 1000,
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: `${promptRecord.content}
|
||
|
||
Return ONLY a valid JSON array of strings.
|
||
Here is the log:
|
||
${log}`,
|
||
},
|
||
],
|
||
}
|
||
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
|
||
const remoteResp = await axios.post(
|
||
remoteUrl + '/api/transferPostData',
|
||
{
|
||
urlAPI: '/api/open-ai-sfp/model-image-info',
|
||
data: payload,
|
||
},
|
||
{
|
||
headers: {
|
||
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
|
||
},
|
||
}
|
||
)
|
||
return remoteResp.data?.Status === 'OK' ? remoteResp.data?.data : ''
|
||
} catch (error: any) {
|
||
console.log('[ERROR] Detect log show env from AI', error)
|
||
}
|
||
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) {
|
||
const isWarningRAM = isRamSufficient(mem, configRam.ram)
|
||
const isWarningFlash = isRamSufficient(flash, configRam.flash)
|
||
if (isWarningRAM || isWarningFlash) {
|
||
const subject = `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Warning RAM, Flash Configuration`
|
||
const body = `
|
||
<p>Station: <b>${this.config.stationName}</b></p>
|
||
<p>Line: <b>${this.config.lineNumber}</b></p>
|
||
<p>Model: <b>${pid}</b></p>
|
||
<p>RAM: ${mem ? `<b>${convertFromKilobytesString(mem)} (<span style="color: ${isWarningRAM ? 'red' : 'black'};">default: ${configRam.ram}</span>)</b>` : ''}</p>
|
||
<p>FLASH: ${flash ? `<b>${convertFromKilobytesString(flash)} (<span style="color: ${isWarningFlash ? 'red' : 'black'};">default: ${configRam.flash}</span>)</b>` : ''}</p>
|
||
<hr />
|
||
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
|
||
${escapeHtml(output)
|
||
.replace('show ver', '')
|
||
.replace('sh ver', '')
|
||
.replace('show version', '')
|
||
.replace('sh version', '')
|
||
.replace(mem, `<span style="color: ${isWarningRAM ? 'red' : 'black'};">${mem}</span>`)
|
||
.replace(
|
||
flash,
|
||
`<span style="color: ${isWarningFlash ? 'red' : 'black'};">${flash}</span>`
|
||
)}</span></div>
|
||
`
|
||
await sendMessageToMail(subject, body)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Send list feature tested
|
||
*/
|
||
sendFeatureTested = async () => {
|
||
this.socketIO.emit('feature_tested', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
listFeatureTested: this.config.listFeatureTested,
|
||
isSkipPhysical: this.config.isSkipPhysical,
|
||
reasonSkipPhysical: this.config.reasonSkipPhysical,
|
||
pid: this.config.inventory?.pid,
|
||
sn: this.config.inventory?.sn,
|
||
vid: this.config.inventory?.vid,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Send summary of all report (DPELP, Physical Testing)
|
||
*/
|
||
sendReportSummary = async (snapshot?: {
|
||
snapConfig: LineConfig
|
||
snapPhysical: PhysicalPortTest
|
||
reason: string
|
||
}) => {
|
||
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 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]
|
||
: config?.inventory
|
||
|
||
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 = config?.latestScenario?.detectAI?.issue || []
|
||
const summary = config?.latestScenario?.detectAI?.summary || ''
|
||
const reason = this.config.reasonSkipPhysical || snapshot?.reason
|
||
const reasonSkipPhysical =
|
||
typeof reason === 'string' && reason.trim().length > 0
|
||
? `<br/><b style="color: #ff0000;">User Skip Test Port</b><br/>
|
||
────────────────────────────────<br/>
|
||
${reason}`
|
||
: ''
|
||
|
||
const body = `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
||
<tr>
|
||
<td style="width: 600px; text-align: center;">DPELP</td>
|
||
<td style="text-align: center;">Physical Testing</td>
|
||
</tr>
|
||
<tr>
|
||
<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/>
|
||
FLASH: <b>${dataShowVersion?.USB_FLASH ? convertFromKilobytesString(dataShowVersion?.USB_FLASH) : ''}</b><br/>
|
||
Licenses: <b>${
|
||
dataShowLic
|
||
? dataShowLic
|
||
?.filter((el) => el.LICENSE_TYPE?.toLowerCase()?.includes('permanent'))
|
||
?.map((v) => v.FEATURE)
|
||
?.join(', ')
|
||
: ''
|
||
}</b><br/>
|
||
Detect from AI: <b style="color: 'black';">${issue?.length ? `<br>- ` + issue.join(`<br>`) : 'No issues detected.'}</b><br/>
|
||
</td>
|
||
<td>
|
||
Total Ports: ${portPhysical?.length}<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: 6;">${missingPoE.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>
|
||
`
|
||
: ''
|
||
}
|
||
${
|
||
missingSFP?.length
|
||
? `
|
||
<br/><b style="color: #ff0000;">Ports Missing SFP</b><br/>
|
||
────────────────────────────────<br/>
|
||
<div style="column-count: 6;">${missingSFP.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>`
|
||
: ''
|
||
}
|
||
${reasonSkipPhysical}
|
||
</td>
|
||
</tr>
|
||
</table>`
|
||
|
||
this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP)
|
||
await sendMessageToMail(
|
||
`[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Summary of Testing Results`,
|
||
body
|
||
)
|
||
this.socketIO.emit('summary_tested', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
body: body,
|
||
title: `[${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Send summary report using the new "Equipment Receiving & Testing Report" template.
|
||
* Email-safe HTML: table-based layout, inline styles, no external CSS or web fonts.
|
||
*/
|
||
sendReportSummaryV2 = async (snapshot?: {
|
||
snapConfig: LineConfig
|
||
snapPhysical: PhysicalPortTest
|
||
reason: string
|
||
outputTestLog: string
|
||
}) => {
|
||
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 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 totalPoE = testedPoE.length + missingPoE.length
|
||
const totalSFP = testedSFP.length + missingSFP.length
|
||
|
||
const showVersion = config?.data?.find(
|
||
(d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver')
|
||
)
|
||
const dataShowVersion =
|
||
showVersion?.textfsm && (showVersion?.textfsm as any)?.[0]
|
||
? (showVersion?.textfsm as any)?.[0]
|
||
: config?.inventory
|
||
|
||
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 as any[])
|
||
: null
|
||
|
||
const issues: string[] = config?.latestScenario?.detectAI?.issue || []
|
||
const skipReason = this.config.reasonSkipPhysical || snapshot?.reason || ''
|
||
const isSkipped = typeof skipReason === 'string' && skipReason.trim().length > 0
|
||
|
||
const verdictPass = missing.length === 0 && issues.length === 0 && !isSkipped
|
||
const verdictLabel = verdictPass ? 'PASSED' : 'NEEDS REVIEW'
|
||
const verdictMsg = verdictPass
|
||
? 'All tests passed — Ready for deployment'
|
||
: 'Issues detected — review required before deployment'
|
||
const verdictBg = verdictPass ? '#ecfdf5' : '#fef2f2'
|
||
const verdictBd = verdictPass ? '#a7f3d0' : '#fecaca'
|
||
const verdictTx = verdictPass ? '#065f46' : '#991b1b'
|
||
|
||
const reportId = `RPT-${config.stationId}-L${config.lineNumber}-${Date.now().toString().slice(-6)}`
|
||
const reportDate = momentTZ()
|
||
.tz(process.env.TIME_ZONE || 'UTC')
|
||
.format('DD MMM YYYY HH:mm')
|
||
|
||
const memText = dataShowVersion?.MEMORY
|
||
? convertFromKilobytesString(dataShowVersion.MEMORY)
|
||
: '—'
|
||
const flashText = dataShowVersion?.USB_FLASH
|
||
? convertFromKilobytesString(dataShowVersion.USB_FLASH)
|
||
: '—'
|
||
|
||
// ---- Template-fallback values (use file's hardcoded content when no real data) ----
|
||
const productName = escapeHtml(String(config?.inventory?.name || ''))
|
||
const productPN = escapeHtml(String(config?.inventory?.pid || ''))
|
||
const productSN = escapeHtml(String(config?.inventory?.sn || ''))
|
||
const productVid = escapeHtml(String(config?.inventory?.vid || ''))
|
||
const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || ''))
|
||
const memDisplay = escapeHtml(memText !== '—' ? memText : '')
|
||
const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '')
|
||
|
||
// AI issue rows (one per real AI issue, fall back to file's hardcoded row when none)
|
||
const aiIssueRowsHtml =
|
||
issues.length > 0
|
||
? issues
|
||
.slice(0, 1)
|
||
.map(
|
||
(issue) =>
|
||
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">★ AI</span><span style="margin-left:8px;vertical-align:middle;">${escapeHtml(issue)}</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
|
||
)
|
||
.join('')
|
||
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">★ AI</span><span style="margin-left:8px;vertical-align:middle;">Potential intermittent power instability. PSU #1 POST logs show 3 retries before handshake.</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
|
||
|
||
// License boxes (real licenses if available, else file's hardcoded boxes)
|
||
const licenseBoxesHtml =
|
||
dataShowLic && dataShowLic.length > 0
|
||
? dataShowLic
|
||
.filter((l) => l.LICENSE_TYPE && l.FEATURE)
|
||
.map(
|
||
(l: any) =>
|
||
`<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">${escapeHtml(String(l.FEATURE || ''))}</div><div style="font-size:10px;color:#9ca3af;">${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}</div></div>`
|
||
)
|
||
.join('')
|
||
: `<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">Network Advantage</div><div style="font-size:10px;color:#9ca3af;">Permanent · Smart License: Active</div></div><div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">DNA Premier</div><div style="font-size:10px;color:#9ca3af;">Evaluation · 85 days remaining</div></div>`
|
||
|
||
// Port stat values (real numbers if any port data, else file's defaults)
|
||
const hasPortData = portPhysical.length > 0
|
||
const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '0/0'
|
||
const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '0/0'
|
||
const poeColor =
|
||
!hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b'
|
||
const sfpColor =
|
||
!hasPortData || (totalSFP > 0 && testedSFP.length === totalSFP) ? '#10b981' : '#f59e0b'
|
||
|
||
// Missing-port detail blocks (only when there is something to show)
|
||
const missingParts: string[] = []
|
||
if (missingPoE.length) {
|
||
missingParts.push(
|
||
`<div style="margin-top:8px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing PoE (${missingPoE.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingPoE.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
|
||
)
|
||
}
|
||
if (missingSFP.length) {
|
||
missingParts.push(
|
||
`<div style="margin-top:6px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing SFP (${missingSFP.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingSFP.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
|
||
)
|
||
}
|
||
if (isSkipped) {
|
||
missingParts.push(
|
||
`<div style="margin-top:6px;padding:8px 12px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:10px;color:#92400e;"><b>User Skipped Physical Test:</b><br/>${escapeHtml(skipReason)}</div>`
|
||
)
|
||
}
|
||
const missingDetailsHtml = missingParts.join('')
|
||
|
||
// Verdict checkmark / cross path
|
||
const verdictPathSvg = verdictPass
|
||
? '<path d="M6.5 10l2.5 2.5 4.5-4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>'
|
||
: '<path d="M7 7l6 6M13 7l-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>'
|
||
|
||
// Physical Check checklist
|
||
const checklistItems: Array<[string, string]> = [
|
||
['ok', 'Packaging intact — no damage to box or foam'],
|
||
['ok', 'No physical damage — chassis, fans, PSU'],
|
||
['ok', `S/N matches label — ${productSN} verified`],
|
||
['ok', 'All 48 GigE + 4 SFP+ ports clean'],
|
||
['ok', 'Accessories — power cable, rack ears, console cable'],
|
||
['warn', 'Minor scratch on top chassis (2cm) — cosmetic only'],
|
||
]
|
||
const checklistRowsHtml = checklistItems
|
||
.map(([k, t]) =>
|
||
k === 'ok'
|
||
? `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#ecfdf5;border:1px solid #a7f3d0;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#065f46;"><span style="display:inline-block;width:18px;height:18px;background:#a7f3d0;color:#065f46;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">✓</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
|
||
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#92400e;"><span style="display:inline-block;width:18px;height:18px;background:#fde68a;color:#92400e;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">!</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
|
||
)
|
||
.join('')
|
||
|
||
// Physical Check photo placeholder cell (4 of these in the photo grid)
|
||
const photoCellHtml = (label: string) =>
|
||
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="border:1px dashed #e5e7eb;border-radius:6px;background:#f9fafb;border-collapse:separate;"><tr><td align="center" style="padding:18px 0;color:#9ca3af;"><svg viewBox="0 0 40 40" width="22" height="22" fill="none" style="display:inline-block;color:#9ca3af;"><rect x="4" y="8" width="32" height="24" rx="3" stroke="currentColor" stroke-width="1.5"/><circle cx="14" cy="18" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M4 28l8-6 6 4 8-8 10 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><div style="font-size:9px;font-weight:600;margin-top:3px;">${label}</div></td></tr></table>`
|
||
|
||
// ---- Body: full template mirroring index.html, table-based + inline styles ----
|
||
const body = `<!DOCTYPE html>
|
||
<html lang="vi">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Equipment Report — Mail Summary</title>
|
||
</head>
|
||
<body style="margin:0;padding:24px 16px 48px;background:#f3f4f6;color:#1a1d23;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.5;">
|
||
|
||
<!-- HEADER + VERDICT -->
|
||
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto 12px;">
|
||
<tr><td>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-bottom:none;border-radius:10px 10px 0 0;border-collapse:separate;">
|
||
<tr><td style="padding:14px 20px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="vertical-align:middle;">
|
||
<table cellpadding="0" cellspacing="0" border="0">
|
||
<tr>
|
||
<td style="background:#1e293b;border-radius:8px;width:34px;height:34px;text-align:center;vertical-align:middle;color:#cbd5e1;padding:6px;">
|
||
<svg viewBox="0 0 32 32" width="22" height="22" fill="none" style="display:block;margin:auto;color:#cbd5e1;"><rect x="2" y="6" width="28" height="20" rx="3" stroke="currentColor" stroke-width="2"/><path d="M8 16h16M8 12h10M8 20h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="24" cy="20" r="2" fill="currentColor"/></svg>
|
||
</td>
|
||
<td style="padding-left:10px;vertical-align:middle;">
|
||
<strong style="font-size:15px;letter-spacing:1.2px;display:block;">PROLOGY IT</strong>
|
||
<span style="font-size:11px;color:#5f6978;font-weight:500;">Equipment Receiving & Testing Report</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
<td align="right" style="vertical-align:middle;">
|
||
<span style="font-size:12px;font-weight:700;color:#3b82f6;font-family:'Courier New',monospace;display:block;">#${escapeHtml(reportId)}</span>
|
||
<span style="font-size:11px;color:#9ca3af;">${escapeHtml(reportDate)}</span>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:${verdictBg};border:1px solid ${verdictBd};border-radius:0 0 10px 10px;border-collapse:separate;">
|
||
<tr><td style="padding:9px 20px;color:${verdictTx};font-size:12px;font-weight:600;">
|
||
<svg viewBox="0 0 20 20" width="18" height="18" fill="none" style="vertical-align:middle;color:${verdictTx};"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.8"/>${verdictPathSvg}</svg>
|
||
<b style="letter-spacing:.8px;vertical-align:middle;margin-left:8px;">${verdictLabel}</b>
|
||
<span style="opacity:.7;font-weight:500;vertical-align:middle;margin-left:8px;">${escapeHtml(verdictMsg)}</span>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
|
||
<!-- MAIN -->
|
||
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto;">
|
||
|
||
<!-- ZONE 1: AT-A-GLANCE — Product Info + Tech Specs -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td width="50%" valign="top" style="padding-right:5px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate; font-size:14px;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Product Info</div>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;width:68px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Name</td><td style="padding:3px 0;font-weight:500;vertical-align:top;"><strong>${productName}</strong></td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">P/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;">${productPN}</td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">S/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;">${productSN}</td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Type</td><td style="padding:3px 0;vertical-align:top;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Switch — Layer 3</span></td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Cond.</td><td style="padding:3px 0;vertical-align:top;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">Refurb — Grade A</span></td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Supplier</td><td style="padding:3px 0;vertical-align:top;font-weight:500;">TechData AU — PO #TD-88432</td></tr>
|
||
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Warranty</td><td style="padding:3px 0;vertical-align:top;font-weight:500;">12 Months (→ May 2027)</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</td>
|
||
<td width="50%" valign="top" style="padding-left:5px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Technical Specs</div>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
|
||
<tr>
|
||
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Specification</th>
|
||
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Actual</th>
|
||
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Default</th>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">IOS-XE Version</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${iosVersion}</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">17.06.01</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">System RAM</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${memDisplay}</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">8 GB</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Flash Storage</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${flashDisplay}</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">16 GB</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Uplink Module</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">C9300-NM-4G</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-style:italic;color:#cbd5e1;font-family:Consolas,monospace;">N/A</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">PSU Model</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">715W AC</td>
|
||
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">715W AC</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:6px 0;font-weight:600;color:#5f6978;">PoE Budget</td>
|
||
<td style="padding:6px 0;font-weight:700;font-family:Consolas,monospace;">437 Watts</td>
|
||
<td style="padding:6px 0;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">437 Watts</td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Issues Found -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Issues Found</div>
|
||
${aiIssueRowsHtml}
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:5px;border-collapse:separate;">
|
||
<tr>
|
||
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#fef3c7;color:#92400e;vertical-align:middle;">COSMETIC</span><span style="margin-left:8px;vertical-align:middle;">Minor scratch on top chassis (2cm) — non-functional</span></td>
|
||
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">Accepted</span></td>
|
||
</tr>
|
||
</table>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:6px;border-collapse:separate;">
|
||
<tr>
|
||
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#ffedd5;color:#9a3412;vertical-align:middle;">MINOR</span><span style="margin-left:8px;vertical-align:middle;">Fan #2 at 48dB under stress (spec 45dB) — within rack tolerance</span></td>
|
||
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Monitor</span></td>
|
||
</tr>
|
||
</table>
|
||
<div style="text-align:center;font-size:11px;color:#9ca3af;padding-top:6px;border-top:1px solid #f0f1f3;"><b>0</b> Critical · <b>0</b> Major · <b>1</b> Minor · <b>1</b> Cosmetic</div>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Receiving & Inspection Notes -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Receiving & Inspection Notes</div>
|
||
<div style="padding:10px 14px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:12px;color:#92400e;margin-bottom:8px;">
|
||
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">⚠ Warning from Warehouse</div>
|
||
<p style="margin:0;">Box arrived with slight indentation on the left corner. Internal foam was still intact. Serial number on box was partially obscured by shipping label but verified upon unboxing.</p>
|
||
</div>
|
||
<div style="padding:10px 14px;background:#f9fafb;border-left:3px solid #e5e7eb;border-radius:0 6px 6px 0;font-size:12px;color:#5f6978;">
|
||
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">Accessory Checklist</div>
|
||
<table cellpadding="0" cellspacing="0" border="0" style="margin-top:6px;">
|
||
<tr>
|
||
<td style="padding:0 4px 0 0;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Rackmount</span></td>
|
||
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>PSU (Internal)</span></td>
|
||
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Console Cable</span></td>
|
||
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Documents</span></td>
|
||
<td style="padding:0 0 0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Original Box</span></td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Inspection Log Workflow -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<!-- line -->
|
||
<tbody style="position:relative; z-index:0;">
|
||
<tr>
|
||
<td colspan="3" style="padding:0 55px;">
|
||
<div style="height:2px;background:#e2e8f0;font-size:0;line-height:0; position:absolute; z-index:-1; width: 90%; top: 12px;">
|
||
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<!-- steps -->
|
||
<tr>
|
||
<!-- Step 1 -->
|
||
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||
✓
|
||
</div>
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||
Received
|
||
</div>
|
||
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||
Trung Nguyen
|
||
</div>
|
||
<div style="font-size:10px;color:#9ca3af;">
|
||
06 May 10:30
|
||
</div>
|
||
</td>
|
||
<!-- Step 2 -->
|
||
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||
✓
|
||
</div>
|
||
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||
Physical Check
|
||
</div>
|
||
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||
Khanh Le
|
||
</div>
|
||
<div style="font-size:10px;color:#9ca3af;">
|
||
06 May 11:15
|
||
</div>
|
||
</td>
|
||
<!-- Step 3 -->
|
||
<td width="34%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||
✓
|
||
</div>
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||
Software Test
|
||
</div>
|
||
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||
Duy Pham (remote)
|
||
</div>
|
||
<div style="font-size:10px;color:#9ca3af;">
|
||
06 May 14:00
|
||
</div>
|
||
</td>
|
||
</tr></tbody>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Divider -->
|
||
<tr><td style="padding:6px 0;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;"> </td>
|
||
<td width="60" align="center" style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#9ca3af;padding:0 12px;">Detail</td>
|
||
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;"> </td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Physical Check section -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
|
||
<tr>
|
||
<td style="padding:7px 12px;color:#166534;font-size:13px;font-weight:700;">
|
||
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#166534;"><rect x="2" y="2" width="16" height="16" rx="3" stroke="currentColor" stroke-width="1.5"/><path d="M7 10h6M10 7v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
<span style="vertical-align:middle;margin-left:8px;">Physical Check</span>
|
||
</td>
|
||
<td align="right" style="padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65;">Khanh Le · 06 May 11:15</td>
|
||
</tr>
|
||
</table>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td width="200" valign="top" style="padding-right:14px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td width="50%" style="padding:0 3px 6px 0;">${photoCellHtml('Front')}</td>
|
||
<td width="50%" style="padding:0 0 6px 3px;">${photoCellHtml('Rear')}</td>
|
||
</tr>
|
||
<tr>
|
||
<td width="50%" style="padding:0 3px 0 0;">${photoCellHtml('S/N Label')}</td>
|
||
<td width="50%" style="padding:0 0 0 3px;">${photoCellHtml('Package')}</td>
|
||
</tr>
|
||
</table>
|
||
</td>
|
||
<td valign="top">
|
||
${checklistRowsHtml}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Software Check section -->
|
||
<tr><td style="padding-bottom:10px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||
<tr><td style="padding:16px 20px;">
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#eff6ff;border:1px solid #93c5fd;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
|
||
<tr>
|
||
<td style="padding:7px 12px;color:#1e40af;font-size:13px;font-weight:700;">
|
||
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#1e40af;"><rect x="2" y="3" width="16" height="11" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M7 17h6M10 14v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
<span style="vertical-align:middle;margin-left:8px;">Software Check</span>
|
||
</td>
|
||
<td align="right" style="padding:7px 12px;color:#1e40af;font-size:11px;font-weight:500;opacity:.65;">Duy Pham (remote) · 06 May 14:00–17:45</td>
|
||
</tr>
|
||
</table>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td width="33%" valign="top" style="padding-right:8px;">
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Hardware Inventory</div>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
|
||
${
|
||
this.config?.inventory?.listInventory
|
||
?.map(
|
||
(item: any) => `
|
||
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">${item.pid}</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">${item.sn}</td></tr>`
|
||
)
|
||
.join('') || ''
|
||
}
|
||
</table>
|
||
</td>
|
||
<td width="33%" valign="top" style="padding:0 4px;">
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">System & License</div>
|
||
${licenseBoxesHtml}
|
||
</td>
|
||
<td width="34%" valign="top" style="padding-left:8px;">
|
||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Port Test Summary</div>
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||
<tr>
|
||
<td width="50%" style="padding:0 3px 6px 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${poeColor};">${escapeHtml(poeText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">${hasPortData ? 'PoE UP' : 'GigE UP'}</div></td></tr></table></td>
|
||
<td width="50%" style="padding:0 0 6px 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${sfpColor};">${escapeHtml(sfpText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">SFP+ UP</div></td></tr></table></td>
|
||
</tr>
|
||
<tr>
|
||
<td width="50%" style="padding:0 3px 0 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#dc2626' : '#10b981'};">${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">PoE+ Test</div></td></tr></table></td>
|
||
<td width="50%" style="padding:0 0 0 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#dc2626' : '#10b981'};">${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">Throughput</div></td></tr></table></td>
|
||
</tr>
|
||
</table>
|
||
${missingDetailsHtml}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<!-- CONSOLE RAW OUTPUT -->
|
||
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top:16px;background:#1e293b;border-radius:6px;border:1px solid #334155;border-collapse:separate;">
|
||
<tr><td style="padding:6px 12px;background:#334155;color:#94a3b8;font-size:10px;font-weight:700;letter-spacing:.5px;border-radius:6px 6px 0 0;">CONSOLE RAW OUTPUT (Boot Log snippet)</td></tr>
|
||
<tr><td><pre style="overflow-y: auto; max-height: 300px; padding:12px;color:#cbd5e1;font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${snapshot?.outputTestLog || 'No test log available'}</pre></td></tr>
|
||
</table>
|
||
</td></tr>
|
||
</table>
|
||
</td></tr>
|
||
|
||
<!-- Footer -->
|
||
<tr><td align="center" style="padding-top:16px;font-size:10px;color:#9ca3af;">Prology IT — Equipment QA System · Confidential — Internal Use Only</td></tr>
|
||
</table>
|
||
|
||
</body>
|
||
</html>`
|
||
|
||
// this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP)
|
||
await sendMessageToMail(
|
||
`[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Summary of Testing Results`,
|
||
body
|
||
)
|
||
this.socketIO.emit('summary_tested', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
body: body,
|
||
title: `[${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 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: [],
|
||
isReady: false,
|
||
}
|
||
this.physicalTest = new PhysicalPortTest([])
|
||
}
|
||
|
||
setTimeoutSendSummaryReport(timeout: number) {
|
||
// 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,
|
||
reason: '',
|
||
outputTestLog: this.outputTestLog,
|
||
}
|
||
this.debounceSendSummaryReport = setTimeout(() => {
|
||
if (!this.config.listFeatureTested?.includes('PHYSICAL')) {
|
||
this.config.isSkipPhysical = true
|
||
this.config.reasonSkipPhysical = 'Timeout, The user has not completed the physical test'
|
||
snapshot.reason = 'Timeout, The user has not completed the physical test'
|
||
}
|
||
this.config.listFeatureTested = ['DPELP', 'PHYSICAL', 'SUMMARY']
|
||
this.sendFeatureTested()
|
||
this.sendReportSummaryV2(snapshot)
|
||
this.outputTestLog = ''
|
||
}, timeout)
|
||
}
|
||
|
||
resetDPELP() {
|
||
this.config.listFeatureTested = []
|
||
this.config.isSkipPhysical = false
|
||
this.config.reasonSkipPhysical = ''
|
||
this.dataDPELP = ''
|
||
this.sendFeatureTested()
|
||
console.log('Reset DPELP data and features', this.config.id, this.config.listFeatureTested)
|
||
}
|
||
|
||
async pingToServer(serverIP: string) {
|
||
this.isPingToServer = true
|
||
this.writeCommand('\r\n')
|
||
this.writeCommand('enable\r\n')
|
||
await sleep(500)
|
||
this.writeCommand(`ping ${serverIP}\r\n`)
|
||
await sleep(500)
|
||
const start = Date.now()
|
||
// console.log('[EXPECT]', expect, timeout)
|
||
while (Date.now() - start < 60000) {
|
||
if (this.outputPingToServer.includes('Success rate')) {
|
||
const match = this.outputPingToServer.match(/Success rate is (\d+) percent/)
|
||
if (match) {
|
||
const rate = Number(match[1])
|
||
if (rate > 0) {
|
||
this.outputPingToServer = ''
|
||
this.isPingToServer = false
|
||
return true
|
||
} else {
|
||
this.isPingToServer = false
|
||
this.outputPingToServer = ''
|
||
this.config.output += '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n'
|
||
this.socketIO.emit('line_output', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data: '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n',
|
||
})
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
await sleep(500)
|
||
}
|
||
this.isPingToServer = false
|
||
this.outputPingToServer = ''
|
||
this.config.output += '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n'
|
||
this.socketIO.emit('line_output', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
data: '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n',
|
||
})
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Config ip address and default gateway for line
|
||
*/
|
||
async configAddressGateway(
|
||
address: string,
|
||
gateway: string,
|
||
portName: string,
|
||
isRouter?: boolean
|
||
) {
|
||
this.config.runningScenario = 'Config Network'
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: 'Config Network',
|
||
})
|
||
await this.writeCommand(`enable\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`configure terminal\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`interface ${portName}\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`ip address ${address} 255.255.0.0\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`no shutdown\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`exit\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(
|
||
!isRouter ? `ip default-gateway ${gateway}\r\n` : `ip route 0.0.0.0 0.0.0.0 ${gateway}`
|
||
)
|
||
await sleep(500)
|
||
await this.writeCommand(`end\r\n`)
|
||
await sleep(500)
|
||
await this.writeCommand(`\r\n`)
|
||
await sleep(1000)
|
||
|
||
this.config.runningScenario = ''
|
||
this.socketIO.emit('running_scenario', {
|
||
stationId: this.config.stationId,
|
||
lineId: this.config.id,
|
||
title: '',
|
||
})
|
||
}
|
||
}
|