diff --git a/BACKEND/app/controllers/healcheck_controller.ts b/BACKEND/app/controllers/healcheck_controller.ts
index 14a4ac6..77241d0 100644
--- a/BACKEND/app/controllers/healcheck_controller.ts
+++ b/BACKEND/app/controllers/healcheck_controller.ts
@@ -54,7 +54,9 @@ export default class HealCheckController {
serialNumberA: dataSN?.serialNumberA,
productModelId: dataSN?.productModelId,
orgId: dataSN?.orgId,
+ condition: dataSN?.condition,
testNotes: dataSN?.testNotes,
+ healthCheck: true,
},
},
{
@@ -71,10 +73,11 @@ export default class HealCheckController {
},
{
...dataCheckNote,
- status: resSN.data?.error ? false : true,
- message: resSN.data?.error
- ? `Checking api update note SN false: '${resSN.data?.error?.message}'`
- : 'Checking api update note SN success',
+ status: resSN?.data?.Status === 'ERROR' ? false : true,
+ message:
+ resSN?.data?.Status === 'ERROR'
+ ? `Checking api update note SN false: '${resSN.data?.Msg}'`
+ : 'Checking api update note SN success',
},
],
}
diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts
index 219d473..c526f96 100644
--- a/BACKEND/app/services/line_connection.ts
+++ b/BACKEND/app/services/line_connection.ts
@@ -19,7 +19,7 @@ import {
updateNoteToERP,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
-import path from 'node:path'
+import path, { join } from 'node:path'
import axios from 'axios'
import redis from '@adonisjs/redis/services/main'
import Line from '#models/line'
@@ -27,6 +27,7 @@ import { 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
@@ -126,6 +127,7 @@ export default class LineConnection {
private session: TestSession
public physicalTest: PhysicalPortTest
private outputPhysicalTest: string
+ private listDeviceIos: string[]
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config
@@ -153,6 +155,7 @@ export default class LineConnection {
this.handleClearLine = handleClearLine
this.physicalTest = new PhysicalPortTest([])
this.outputPhysicalTest = ''
+ this.listDeviceIos = []
}
connect(timeoutMs = 5000) {
@@ -922,10 +925,10 @@ export default class LineConnection {
${r.rule} |
${r.message} |
-
-
- ${escapeHtml(r.log.trim())}
- |
+ *${escapeHtml(r.log.trim())
+ .split('*')
+ .filter((el) => el)
+ .join(' *')} |
`
)
@@ -939,26 +942,10 @@ export default class LineConnection {
`
}
- renderAIDetectTable(row: any): string {
- return `
-
-
- | Summary |
- Issues |
-
-
- | ${row.summary || ''} |
- ${row.issues?.length ? `- ` + row.issues.join(` - `) : '- No issues detected.'} |
-
-
- `
- }
-
buildEmailContent(result: TestResult): string {
const rows = mapErrorsToRows(result.errors)
const table = this.renderErrorTable(rows)
- // const tableAI = this.renderAIDetectTable(value)
-
+ console.log(table)
return `
Cisco Device Log Result
Line: ${this.config.lineNumber} - Station: ${this.config.stationName}
@@ -1034,7 +1021,7 @@ export default class LineConnection {
async getPorts(): Promise {
this.writeCommand(' show power inline\r\n')
this.writeCommand(' \r\n')
- await this.sleep(3000)
+ await this.sleep(5000)
const statusOutput = this.outputPhysicalTest
this.outputPhysicalTest = ''
@@ -1107,14 +1094,14 @@ export default class LineConnection {
},
{
expect: 'rommon',
- send: `tftpdnld`,
+ send: this.listDeviceIos?.includes(nameIos) ? '' : `tftpdnld`,
delay: '1',
repeat: '1',
note: '',
},
{
- expect: 'y/n',
- send: `y`,
+ expect: this.listDeviceIos?.includes(nameIos) ? '' : 'y/n',
+ send: this.listDeviceIos?.includes(nameIos) ? '' : `y`,
delay: '2',
repeat: '1',
note: '',
@@ -1200,9 +1187,10 @@ export default class LineConnection {
timeout: 1000,
body: JSON.stringify(body),
}
+
await sleep(5000)
await this.runScript(script as any, userName)
- await this.endEmailLoadIos(nameIos, startTime)
+ await this.sendEmailLoadIos(nameIos, startTime)
}
async loadIosSwitch(nameIos: string, userName: string) {
@@ -1213,6 +1201,8 @@ export default class LineConnection {
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')
+ await this.backupIos(nameIos)
+
const body = [
{
expect: '',
@@ -1286,21 +1276,21 @@ export default class LineConnection {
},
{
expect: '#',
- send: `copy tftp: flash:`,
+ send: this.listDeviceIos?.includes(nameIos) ? '' : `copy tftp: flash:`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
- send: `${tftpIp}`,
+ send: this.listDeviceIos?.includes(nameIos) ? '' : `${tftpIp}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
- send: `ios/${nameIos}`,
+ send: this.listDeviceIos?.includes(nameIos) ? '' : `ios/${nameIos}`,
delay: '1',
repeat: '1',
note: '',
@@ -1409,10 +1399,10 @@ export default class LineConnection {
}
await this.runScript(script as any, userName)
- await this.endEmailLoadIos(nameIos, startTime)
+ await this.sendEmailLoadIos(nameIos, startTime)
}
- async endEmailLoadIos(nameIos: string, startTime: string) {
+ 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 = `
@@ -1431,4 +1421,100 @@ export default class LineConnection {
body
)
}
+
+ async checkDeviceFlash() {
+ this.writeCommand(' enable\r\n')
+ this.writeCommand('show flash:\r\n')
+ await sleep(2000)
+ const ios = []
+ const binRegex = /^\s*\d+\s+-rwx\s+\d+\s+.*?\s+([^\s]+\.bin)\s*$/gim
+
+ let match
+ while ((match = binRegex.exec(this.outputBuffer)) !== null) {
+ ios.push(match[1])
+ }
+ return ios
+ }
+
+ 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)
+ }
+
+ 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(`ios/${fileName}\r\n`)
+ await sleep(5000)
+ while (true) {
+ if (this.outputBuffer.includes('#')) {
+ this.outputBuffer = ''
+ this.config.runningScenario = ''
+ return true
+ }
+ await sleep(5000)
+ }
+ }
+
+ // function get list ios
+ async getListIos() {
+ try {
+ const controller = new IosLicenseController()
+ const listIos = await controller.getIos()
+ return listIos
+ } catch (error) {
+ console.log('Error get ios', error)
+ return []
+ }
+ }
+
+ 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
+ }
+
+ 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?.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)
+ }
}
diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts
index e00a94a..7c90d86 100644
--- a/BACKEND/app/services/physical_test_service.ts
+++ b/BACKEND/app/services/physical_test_service.ts
@@ -1,8 +1,11 @@
import moment from 'moment'
import { normalizeInterface } from '../ultils/helper.js'
import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
-const LINK_UPDOWN_REGEX =
+const LINK_REGEX =
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
+const POE_GRANTED_REGEX = /%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i
+const POE_DISCONNECT_REGEX =
+ /%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i
export class PhysicalPortTest {
public ports = new Map()
@@ -44,24 +47,44 @@ export class PhysicalPortTest {
}
handleLog(line: string) {
- const match = line.match(LINK_UPDOWN_REGEX)
- if (!match) return
+ let iface: string | null = null
+ let markTested = false
+ let state: 'up' | 'down' | undefined
- const rawIface = match[1]
- const state = match[2] as 'up' | 'down'
- const iface = normalizeInterface(rawIface)
+ // 1️⃣ LINK / LINEPROTO
+ let match = line.match(LINK_REGEX)
+ if (match) {
+ iface = normalizeInterface(match[1])
+ state = match[2] as 'up' | 'down'
+ if (state === 'up') markTested = true
+ }
+
+ // 2️⃣ POE POWER GRANTED
+ match = line.match(POE_GRANTED_REGEX)
+ if (match) {
+ iface = normalizeInterface(match[1])
+ markTested = true
+ }
+
+ // 3️⃣ POE DISCONNECT
+ match = line.match(POE_DISCONNECT_REGEX)
+ if (match) {
+ iface = normalizeInterface(match[1])
+ markTested = true
+ }
+
+ if (!iface) return
const port = this.ports.get(iface)
if (!port) return
- // tránh update trùng state liên tiếp
- if (port.lastState === state) return
-
- port.lastState = state
port.lastSeen = new Date()
- // chỉ cần UP 1 lần là pass
- if (state === 'up' && !port.tested) {
+ if (state && port.lastState === state) return
+ if (state) port.lastState = state
+
+ // ⭐ PASS nếu có ít nhất 1 event hợp lệ
+ if (markTested && !port.tested) {
port.tested = true
this.checkDone()
}
@@ -141,7 +164,7 @@ export class PhysicalPortTest {
Serial Number : ${report.device.serial ?? 'N/A'}
Started At : ${moment(report.startTime).format('YYYY/MM/DD, HH:mm:ss')}
Finished At : ${moment(report.endTime).format('YYYY/MM/DD, HH:mm:ss')}
- Duration : ${Math.floor(report.durationMs / 1000)} sec
+ Duration : ${this.formatDuration(report.durationMs)}
Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}
────────────────────────────────
@@ -169,4 +192,12 @@ export class PhysicalPortTest {
`.trim()
}
+
+ formatDuration(ms: number): string {
+ const totalSeconds = Math.floor(ms / 1000)
+ const minutes = Math.floor(totalSeconds / 60)
+ const seconds = totalSeconds % 60
+
+ return `${minutes}m ${seconds}s`
+ }
}
diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts
index 7c8ce81..2e2ac56 100644
--- a/BACKEND/app/ultils/helper.ts
+++ b/BACKEND/app/ultils/helper.ts
@@ -8,8 +8,8 @@ import axios from 'axios'
import moment from 'moment'
const mailTo = 'andrew.ng@apactech.io'
-const mailCC = ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
-// const mailCC = ''
+// const mailCC = ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
+const mailCC = ''
type DetectAI = {
status: string[]
diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts
index 2d2c664..d25ebe5 100644
--- a/BACKEND/app/ultils/types.ts
+++ b/BACKEND/app/ultils/types.ts
@@ -61,6 +61,8 @@ export interface PortState {
tested: boolean
lastState?: 'up' | 'down'
lastSeen?: Date
+ poeGranted?: boolean
+ poeDisconnected?: boolean
}
export interface PhysicalTestResult {
diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts
index 45a4be1..716933f 100644
--- a/BACKEND/providers/socket_io_provider.ts
+++ b/BACKEND/providers/socket_io_provider.ts
@@ -663,13 +663,31 @@ export class WebSocketIo {
})
socket.on('load_ios_router', async (data) => {
- const { stationId, lineId, iosName } = data
+ const { stationId, lineId, iosName, outletNumber, station, apcName, isReboot } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => {
- lineCon.loadIosRouter(iosName, userName)
+ await lineCon.backupIos(iosName)
+ if (isReboot) {
+ if (!outletNumber || outletNumber < 0) return
+ if (!station) return
+ const apcIp = (station as any)[`${apcName}_ip`] as string
+ if (!this.apcsControl.get(apcIp)) await this.connectApc(io, apcName, station)
+ const apc = this.apcsControl.get(apcIp)
+ if (apc && apc.status !== 'CONNECTED') {
+ await apc.reconnect()
+ this.keepConnectAPC(apcIp, io)
+ }
+ if (apc) {
+ await apc?.restartOutlet(outletNumber)
+ setTimeout(() => {
+ apc?.navigateToOutlets()
+ }, 10000)
+ }
+ }
+ await lineCon.loadIosRouter(iosName, userName)
},
{}
)
diff --git a/FRONTEND/src/components/Modal/ModalSelectIOS.tsx b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx
index 3d7351e..622cefb 100644
--- a/FRONTEND/src/components/Modal/ModalSelectIOS.tsx
+++ b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx
@@ -153,18 +153,16 @@ const ModalSelectIOS = ({
size="sm"
leftSection={}
onClick={() => {
- if (isReboot)
- socket?.emit("control_apc", {
- outletNumbers: [line?.outlet],
- station: { ...station, lines: [] },
- action: "restart",
- apcName: line?.apc_name || line?.apcName,
- });
- socket?.emit("load_ios_router", {
+ const payload = {
stationId: Number(station?.id),
lineId: Number(line?.id),
iosName: ios,
- });
+ station: station,
+ outletNumber: line?.outlet || -1,
+ apcName: line?.apcName || line?.apc_name,
+ isReboot: isReboot,
+ };
+ socket?.emit("load_ios_router", payload);
close();
}}
>
diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx
index 77f5660..b7be0f5 100644
--- a/FRONTEND/src/components/Modal/ModalTerminal.tsx
+++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx
@@ -1032,7 +1032,7 @@ const ModalTerminal = ({
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
- }, 4000);
+ }, 10000);
}}
>
Start Physical Test