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 || "") : "") : ""}