diff --git a/BACKEND/.gitignore b/BACKEND/.gitignore
index 3e126c0..11da96f 100644
--- a/BACKEND/.gitignore
+++ b/BACKEND/.gitignore
@@ -25,3 +25,5 @@ yarn-error.log
.DS_Store
storage/system_logs
+storage/ios
+storage/license
diff --git a/BACKEND/app/controllers/ios_license_controller.ts b/BACKEND/app/controllers/ios_license_controller.ts
new file mode 100644
index 0000000..62a135a
--- /dev/null
+++ b/BACKEND/app/controllers/ios_license_controller.ts
@@ -0,0 +1,81 @@
+import type { HttpContext } from '@adonisjs/core/http'
+import fs from 'node:fs'
+import path from 'node:path'
+
+export default class IosLicenseController {
+ /* ================= LIST ================= */
+
+ async getIos() {
+ return fs.readdirSync('storage/ios')
+ }
+
+ async getLicense() {
+ return fs.readdirSync('storage/license')
+ }
+
+ /* ================= UPLOAD ================= */
+
+ async uploadIos({ request, response }: HttpContext) {
+ const file = request.file('file', {
+ size: '4gb',
+ extnames: ['bin', 'img', 'tar'],
+ })
+
+ if (!file) {
+ return response.badRequest('File is required')
+ }
+
+ await file.move('storage/ios', {
+ name: file.clientName,
+ overwrite: true,
+ })
+
+ return {
+ success: true,
+ filename: file.clientName,
+ }
+ }
+
+ async uploadLicense({ request, response }: HttpContext) {
+ const file = request.file('file', {
+ size: '100mb',
+ extnames: ['lic', 'txt'],
+ })
+
+ if (!file) {
+ return response.badRequest('File is required')
+ }
+
+ await file.move('storage/license', {
+ name: file.clientName,
+ overwrite: true,
+ })
+
+ return {
+ success: true,
+ filename: file.clientName,
+ }
+ }
+
+ /* ================= DOWNLOAD ================= */
+
+ async downloadIos({ params, response }: HttpContext) {
+ const filePath = path.join('"storage/ios"', params.filename)
+
+ if (!fs.existsSync(filePath)) {
+ return response.notFound('File not found')
+ }
+
+ return response.download(filePath)
+ }
+
+ async downloadLicense({ params, response }: HttpContext) {
+ const filePath = path.join('"storage/license"', params.filename)
+
+ if (!fs.existsSync(filePath)) {
+ return response.notFound('File not found')
+ }
+
+ return response.download(filePath)
+ }
+}
diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts
index 0872fac..8fbf735 100644
--- a/BACKEND/app/services/line_connection.ts
+++ b/BACKEND/app/services/line_connection.ts
@@ -24,7 +24,6 @@ import axios from 'axios'
import redis from '@adonisjs/redis/services/main'
import Line from '#models/line'
import { ErrorRow, TestResult } from '../ultils/types.js'
-import moment from 'moment'
import momentTZ from 'moment-timezone'
import { PhysicalPortTest } from './physical_test_service.js'
@@ -359,7 +358,7 @@ export default class LineConnection {
console.log(
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
)
- this.config.runningScenario = ''
+ this.config.runningScenario = script?.title
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
@@ -473,6 +472,12 @@ export default class LineConnection {
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
+ }
item.textfsm = JSON.parse(item.textfsm)
}
})
@@ -646,6 +651,8 @@ export default class LineConnection {
const start = Date.now()
// console.log('[EXPECT]', expect, timeout)
while (Date.now() - start < timeout) {
+ console.log(expect)
+ console.log(this.outputBuffer)
if (this.outputBuffer.includes(expect)) {
this.outputBuffer = ''
return true
@@ -662,7 +669,15 @@ export default class LineConnection {
if (item?.textfsm && isValidJson(item?.textfsm)) {
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
const dataInventory = JSON.parse(item.textfsm)[0]
- this.config.inventory = dataInventory
+ this.config.inventory = this.config.inventory
+ ? { ...this.config.inventory, ...dataInventory }
+ : dataInventory
+ }
+ 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
}
item.textfsm = JSON.parse(item.textfsm)
}
@@ -855,15 +870,13 @@ export default class LineConnection {
// console.log(detectLog)
const tableHTML = this.buildEmailContent(result)
await sendMessageToMail(
- 'andrew.ng@apactech.io',
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue`,
tableHTML +
`${`
Logs:
- ${this.bufferLog.allBuffer}
`}`,
- ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
+ ${this.bufferLog.allBuffer}`}`
)
this.session.clear()
this.bufferLog.clear()
@@ -962,12 +975,6 @@ export default class LineConnection {
}
this.config.runningPhysical = true
this.config.runningScenario = 'Physical Test'
- this.socketIO.emit('running_scenario', {
- stationId: this.config.stationId,
- lineId: this.config.id,
- title: 'Physical Test',
- physical: true,
- })
const listPorts = await this.getPorts()
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
@@ -982,16 +989,17 @@ export default class LineConnection {
return
}
- this.physicalTest.start(listPorts)
- const interval = setInterval(async () => {
- if (!this.physicalTest.done) {
- const result = this.physicalTest.getResult()
- // console.warn('⚠️ Missing ports:', result.missingPorts)
- } else {
- clearInterval(interval)
- this.endTesting()
- }
- }, 10000)
+ this.physicalTest.start(listPorts, this.config.inventory)
+ // const interval = setInterval(async () => {
+ // if (!this.physicalTest.done) {
+ // // const result = this.physicalTest.getResult()
+ // // console.warn('⚠️ Missing ports:', result.missingPorts)
+ // } else {
+ // clearInterval(interval)
+ // await this.sendReportPhysicalTest()
+ // this.endTesting()
+ // }
+ // }, 10000)
}
endTesting() {
@@ -1030,4 +1038,12 @@ export default class LineConnection {
this.config.ports = [...new Set(ports)]
return [...new Set(ports)]
}
+
+ async sendReportPhysicalTest() {
+ const formReport = this.physicalTest.getFormReport()
+ await sendMessageToMail(
+ `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Port Test`,
+ formReport
+ )
+ }
}
diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts
index 75a0c29..e86ba76 100644
--- a/BACKEND/app/services/physical_test_service.ts
+++ b/BACKEND/app/services/physical_test_service.ts
@@ -1,5 +1,6 @@
+import moment from 'moment'
import { normalizeInterface } from '../ultils/helper.js'
-import { PhysicalTestResult, PortState } from '../ultils/types.js'
+import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
const LINK_UPDOWN_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
@@ -7,9 +8,13 @@ export class PhysicalPortTest {
public ports = new Map()
private expectedPorts: string[]
public done = false
+ private startTime: Date
+ public inventory: any
constructor(expectedPorts: string[]) {
this.expectedPorts = expectedPorts
+ this.startTime = new Date()
+ this.inventory = ''
expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), {
@@ -19,9 +24,11 @@ export class PhysicalPortTest {
})
}
- start(expectedPorts: string[]) {
+ start(expectedPorts: string[], inventory: any) {
this.ports.clear()
+ this.startTime = new Date()
this.expectedPorts = expectedPorts
+ this.inventory = inventory
this.done = false
expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), {
@@ -79,10 +86,26 @@ export class PhysicalPortTest {
}
onDone() {
- this.ports.clear()
+ this.getFormReport()
+ // this.ports.clear()
console.log('✅ Physical Test DONE')
}
+ getFormReport() {
+ const report: PhysicalTestReport = {
+ device: {
+ model: this?.inventory?.pid || '',
+ serial: this?.inventory?.sn || '',
+ },
+ startTime: this.startTime,
+ endTime: new Date(),
+ durationMs: Date.now() - this.startTime.getTime(),
+ ports: Array.from(this.ports.values()),
+ }
+ return this.generateEmailReport(report)
+ // console.log('✅ Physical Test DONE')
+ }
+
getResult(): PhysicalTestResult {
const tested = [...this.ports.values()].filter((p) => p.tested)
const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name)
@@ -94,4 +117,46 @@ export class PhysicalPortTest {
status: this.done ? 'DONE' : 'RUNNING',
}
}
+
+ generateEmailReport(report: PhysicalTestReport): string {
+ const tested = report.ports.filter((p) => p.tested)
+ const missing = report.ports.filter((p) => !p.tested)
+
+ const status = missing.length === 0 ? 'PASS' : 'WARNING'
+
+ return `
+ Physical Port Test Report
+ ────────────────────────────────
+ Model : ${report.device.model ?? 'N/A'}
+ 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
+ Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}
+
+ ────────────────────────────────
+ Test Summary
+ ────────────
+ Total Ports : ${report.ports.length}
+ Ports Tested (UP) : ${tested.length}
+ Ports Missing : ${missing.length}
+
+ ────────────────────────────────
+ Passed Ports
+ ────────────
+ ${tested.map((p) => p.name).join('
')}
+
+ ${
+ missing.length
+ ? `
+ ────────────────────────────────
+ Missing Ports
+ ─────────────
+ ${missing.map((p) => p.name).join('
')}
+ `
+ : ''
+ }
+
+ `.trim()
+ }
}
diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts
index 9aab582..2e2ac56 100644
--- a/BACKEND/app/ultils/helper.ts
+++ b/BACKEND/app/ultils/helper.ts
@@ -7,6 +7,10 @@ import { ErrorRow, LogRule, ParsedLog, TestError, TestResult } from './types.js'
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 = ''
+
type DetectAI = {
status: string[]
issue: string[]
@@ -221,12 +225,7 @@ export function mapToLineFormat(input: InputData) {
}
}
-export function sendMessageToMail(
- email: string,
- subject: string,
- text: string,
- cc?: string[]
-): Promise {
+export function sendMessageToMail(subject: string, text: string): Promise {
return new Promise((resolve, reject) => {
const transporter = nodeMailer.createTransport({
pool: true,
@@ -241,10 +240,10 @@ export function sendMessageToMail(
const mailOptions = {
from: process.env.SMTP_USERNAME,
- to: email,
+ to: mailTo,
subject,
html: text,
- cc: cc,
+ cc: mailCC,
}
transporter.sendMail(mailOptions, (error: any, info: any) => {
diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts
index 2483817..2d2c664 100644
--- a/BACKEND/app/ultils/types.ts
+++ b/BACKEND/app/ultils/types.ts
@@ -69,3 +69,14 @@ export interface PhysicalTestResult {
missingPorts: string[]
status: 'RUNNING' | 'DONE' | 'WARNING'
}
+
+export interface PhysicalTestReport {
+ device: {
+ model?: string
+ serial?: string
+ }
+ startTime: Date
+ endTime: Date
+ durationMs: number
+ ports: PortState[]
+}
diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts
index 52f5398..37f0f6a 100644
--- a/BACKEND/providers/socket_io_provider.ts
+++ b/BACKEND/providers/socket_io_provider.ts
@@ -586,10 +586,8 @@ export class WebSocketIo {
titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat,
})
await sendMessageToMail(
- 'andrew.ng@apactech.io',
`[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`,
- tableHTML,
- ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
+ tableHTML
)
await sendMessageToZulip(
'stream',
@@ -631,7 +629,10 @@ export class WebSocketIo {
io,
stationId,
[lineId],
- async (lineCon) => lineCon.endTesting(),
+ async (lineCon) => {
+ lineCon.endTesting()
+ await lineCon.sendReportPhysicalTest()
+ },
{}
)
})
@@ -895,7 +896,13 @@ export class WebSocketIo {
this.lineMap.forEach((line, id) => {
if (line && line.config) {
newMap.set(id, {
- config: { ...line.config, status: 'disconnected' },
+ config: {
+ ...line.config,
+ status: 'disconnected',
+ userEmailOpenCLI: '',
+ userOpenCLI: '',
+ openCLI: false,
+ },
} as LineConnection)
}
})
diff --git a/BACKEND/start/routes.ts b/BACKEND/start/routes.ts
index 5442557..b67df6d 100644
--- a/BACKEND/start/routes.ts
+++ b/BACKEND/start/routes.ts
@@ -105,3 +105,15 @@ router
router.get('/', '#controllers/healcheck_controller.check')
})
.prefix('atc/health-check')
+
+router
+ .group(() => {
+ router.get('/ios', '#controllers/ios_license_controller.getIos')
+ router.post('/ios/upload', '#controllers/ios_license_controller.uploadIos')
+ router.get('/ios/download/:filename', '#controllers/ios_license_controller.downloadIos')
+
+ router.get('/license', '#controllers/ios_license_controller.getLicense')
+ router.post('/license/upload', '#controllers/ios_license_controller.uploadLicense')
+ router.get('/license/download/:filename', '#controllers/ios_license_controller.downloadLicense')
+ })
+ .prefix('/api')
diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx
index 20f482f..e16ad89 100644
--- a/FRONTEND/src/App.tsx
+++ b/FRONTEND/src/App.tsx
@@ -428,17 +428,15 @@ function App() {
lines: station.lines.map((line) => {
const buffered = lineBuffersRef.current.get(line.id || 0);
if (!buffered) return line; // không có update
- updateValueSelectedLine(line?.id || 0, {
- netOutput: buffered,
- loadingClearTerminal: false,
- });
- return {
+ const data = {
...line,
netOutput: (line.netOutput || "") + buffered,
output: buffered,
loadingOutput: line.loadingOutput ? false : true,
loadingClearTerminal: false,
};
+ updateValueSelectedLine(line?.id || 0, data);
+ return data;
}),
}))
);
@@ -507,28 +505,16 @@ function App() {
[]
);
- const updateValueSelectedLine = useCallback(
- (lineId: number, updates: Partial) => {
- // Update selectedLine nếu nó đang được chọn
- setSelectedLine((prevSelected) => {
- if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
-
- const isNetOutput = typeof updates?.netOutput !== "undefined";
-
- return {
- ...prevSelected,
- ...updates,
- ...(isNetOutput && {
- netOutput:
- (prevSelected.netOutput || "") + (updates.netOutput || ""),
- output: updates.netOutput,
- loadingOutput: prevSelected.loadingOutput ? false : true,
- }),
- };
- });
- },
- []
- );
+ const updateValueSelectedLine = (lineId: number, updates: Partial) => {
+ // Update selectedLine nếu nó đang được chọn
+ setSelectedLine((prevSelected) => {
+ if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
+ return {
+ ...prevSelected,
+ ...updates,
+ };
+ });
+ };
// const getLine = (lineId: number, stationId: number) => {
// const station = stations?.find((sta) => sta.id === stationId);
diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx
index 3780569..61dd240 100644
--- a/FRONTEND/src/components/Modal/ModalTerminal.tsx
+++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx
@@ -432,7 +432,7 @@ const ModalTerminal = ({
);
return showVersion?.textfsm && showVersion?.textfsm?.[0]
? showVersion?.textfsm?.[0]
- : null;
+ : line?.inventory;
};
const findDataShowLicense = () => {
@@ -746,9 +746,9 @@ const ModalTerminal = ({
{findDataShowVersion()
- ? findDataShowVersion()?.MEMORY +
+ ? (findDataShowVersion()?.MEMORY || "") +
(findDataShowVersion()?.USB_FLASH
- ? " - " + findDataShowVersion()?.USB_FLASH
+ ? " - " + (findDataShowVersion()?.USB_FLASH || "")
: "")
: ""}