Updaet wiki

This commit is contained in:
nguyentrungthat 2025-12-03 11:37:31 +07:00
parent ae01929bf2
commit 8cf801d964
6 changed files with 1601 additions and 84 deletions

View File

@ -89,7 +89,7 @@ interface DataDPELP {
pid: any pid: any
vid: any vid: any
sn: any sn: any
mode: string ios: string
mac: string mac: string
license: any license: any
issues: string[] issues: string[]
@ -121,7 +121,16 @@ export default class LineConnection {
this.outputInventory = '' this.outputInventory = ''
this.outputScenario = '' this.outputScenario = ''
// this.bufferCommand = '' // this.bufferCommand = ''
this.dataDPELP = 'No data' this.dataDPELP = {
line: this.config.lineNumber,
pid: '',
vid: '',
sn: '',
ios: '',
mac: '',
license: [],
issues: ['No data'],
}
this.retryConnect = 0 this.retryConnect = 0
this.handleClearLine = handleClearLine this.handleClearLine = handleClearLine
} }
@ -212,8 +221,6 @@ export default class LineConnection {
}) })
this.client.on('close', async () => { this.client.on('close', async () => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`) console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected' this.config.status = 'disconnected'
// this.config.inventory = undefined // this.config.inventory = undefined
@ -223,14 +230,14 @@ export default class LineConnection {
lineNumber, lineNumber,
status: 'disconnected', status: 'disconnected',
}) })
if (this.retryConnect <= 5) { // if (this.retryConnect <= 5) {
await this.sleep(5000) // await this.sleep(5000)
console.log(`Retry connect line [${this.config.lineNumber}] times`, this.retryConnect) // console.log(`Retry connect line [${this.config.lineNumber}] times`, this.retryConnect)
this.retryConnect += 1 // this.retryConnect += 1
await this.reconnect() // await this.reconnect()
} else { // } else {
this.retryConnect = 0 // this.retryConnect = 0
} // }
}) })
this.client.on('timeout', () => { this.client.on('timeout', () => {
@ -265,9 +272,9 @@ export default class LineConnection {
async writeCommand(cmd: string | Buffer<ArrayBuffer>, userName = '') { async writeCommand(cmd: string | Buffer<ArrayBuffer>, userName = '') {
if (this.client.destroyed) { if (this.client.destroyed) {
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`) console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
this.disconnect() // this.disconnect()
await sleep(2000) // await sleep(2000)
await this.connect() // await this.connect()
return return
} }
@ -301,7 +308,8 @@ export default class LineConnection {
async disconnect() { async disconnect() {
try { try {
this.handleClearLine() console.log('[DISCONNECT] Line', this.config.lineNumber)
// this.handleClearLine()
this.client.destroy() this.client.destroy()
this.config.status = 'disconnected' this.config.status = 'disconnected'
this.socketIO.emit('line_disconnected', { this.socketIO.emit('line_disconnected', {
@ -731,24 +739,22 @@ export default class LineConnection {
role: 'user', role: 'user',
content: `Bạn là chuyên gia phân tích log thiết bị mạng. content: `Bạn là chuyên gia phân tích log thiết bị mạng.
Hãy phân tích đoạn log sau xuất kết quả theo đúng format: Hãy phân tích đoạn log sau xuất kết quả theo đúng format:
${log}
Yêu cầu đu ra đúng cấu trúc: Yêu cầu đu ra đúng cấu trúc:
status:
(Tóm tắt trạng thái tổng thể của hệ thống trong 24 ý)
issue: issue:
(Tóm tắt cực ngắn gọn các lỗi/dấu hiệu bất thường, mỗi vấn đ 1 dòng, không bắt các vấn đ không quan trọng như về port changed state to up/down hay Invalid input, Incomplete command) (Tóm tắt ngắn gọn các lỗi/dấu hiệu bất thường, mỗi vấn đ 1 dòng, bỏ qua các vấn đ không quan trọng như về port up/down hay Invalid input, Incomplete command)
Quy tắc: Quy tắc:
Không giải thích dài dòng. Không giải thích dài dòng.
Chỉ tập trung vào lỗi, cảnh báo, thay đi trạng thái up/down bất thường. tập trung vào lỗi phần cứng, cảnh báo bất thường
Nếu log không lỗi ghi "No issues detected.". Nếu log không lỗi ghi "No issues detected.".
Ngắn gọn, dễ đc, đúng format Ngắn gọn, dễ đc, đúng format
Return only json format with English`, Return only json format with English.
Here is the log:
${log}
`,
}, },
], ],
} }

View File

@ -1,5 +1,6 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import nodeMailer from 'nodemailer'
type DetectAI = { type DetectAI = {
status: string[] status: string[]
@ -15,6 +16,9 @@ type InputData = {
data?: any[] data?: any[]
} }
// Types
type SendMailResponse = string
/** /**
* Function to clean up unwanted characters from the output data. * Function to clean up unwanted characters from the output data.
* @param {string} data - The raw data to be cleaned. * @param {string} data - The raw data to be cleaned.
@ -150,7 +154,7 @@ export function mapToLineFormat(input: InputData) {
pid: '', pid: '',
vid: '', vid: '',
sn: '', sn: '',
mode: '', ios: '',
mac: '', mac: '',
license: [], license: [],
issues: ['No data'], issues: ['No data'],
@ -159,21 +163,27 @@ export function mapToLineFormat(input: InputData) {
// MAC // MAC
let mac = '' let mac = ''
let ios = ''
const showVersion = input.data?.find((d) => d.command === 'show version') const showVersion = input.data?.find((d) => d.command === 'show version')
if (showVersion?.textfsm?.[0]?.MAC_ADDRESS) { if (showVersion?.textfsm?.[0]?.MAC_ADDRESS) {
mac = showVersion.textfsm[0].MAC_ADDRESS mac = showVersion.textfsm[0].MAC_ADDRESS
} }
if (showVersion?.textfsm?.[0]?.SOFTWARE_IMAGE) {
ios = showVersion.textfsm[0].SOFTWARE_IMAGE + ' ' + (showVersion?.textfsm?.[0]?.VERSION || '')
}
// License // License
const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license') const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license')
const license = const license =
dataLicense?.textfsm && Array.isArray(dataLicense.textfsm) dataLicense?.textfsm && Array.isArray(dataLicense.textfsm)
? dataLicense.textfsm.map((v: any) => v.FEATURE) ? dataLicense.textfsm
?.filter((el: any) => el.LICENSE_TYPE === 'Permanent')
.map((v: any) => v.FEATURE)
: '' : ''
// Mode (DPEL / DPELP) // // Mode (DPEL / DPELP)
const dataPlatform = input.data?.find((el) => el.command?.trim() === 'show platform') // const dataPlatform = input.data?.find((el) => el.command?.trim() === 'show platform')
const mode = dataPlatform && !dataPlatform.output?.includes('Incomplete') ? 'DPELP' : 'DPEL' // const mode = dataPlatform && !dataPlatform.output?.includes('Incomplete') ? 'DPELP' : 'DPEL'
// Issues // Issues
const issues = Array.isArray(input.latestScenario?.detectAI?.issue) const issues = Array.isArray(input.latestScenario?.detectAI?.issue)
@ -187,9 +197,47 @@ export function mapToLineFormat(input: InputData) {
pid, pid,
vid, vid,
sn, sn,
mode, ios,
mac, mac,
license, license,
issues, issues,
} }
} }
export function sendMessageToMail(
email: string,
subject: string,
text: string,
cc?: string[]
): Promise<SendMailResponse> {
return new Promise((resolve, reject) => {
const transporter = nodeMailer.createTransport({
pool: true,
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
},
})
const mailOptions = {
from: process.env.SMTP_USERNAME,
to: email,
subject,
html: text,
cc: cc,
}
transporter.sendMail(mailOptions, (error: any, info: any) => {
if (error) {
console.error(error)
reject(error)
} else {
console.log('Email sent: ' + info.response)
resolve(info.response)
}
})
})
}

View File

@ -1,31 +1,33 @@
import XRegExp from 'xregexp' import XRegExp from 'xregexp'
// Patterns for each field
// Parser function
const parseLog = (data: string) => { const parseLog = (data: string) => {
const patterns = [ const patterns = [
XRegExp('^Index\\s+\\d+\\s+Feature:\\s+(?<FEATURE>\\S+)'), XRegExp('^Index\\s+(?<INDEX>\\d+)\\s+Feature:\\s*(?<FEATURE>.*)$'),
XRegExp('^Period\\s+left:\\s+(?<PERIOD_LEFT>.+)'), XRegExp('^Period\\s+left:\\s*(?<PERIOD_LEFT>.*)$'),
XRegExp('^Period\\s+Used:\\s+(?<PERIOD_USED>.+)'), XRegExp('^Period\\s+Used:\\s*(?<PERIOD_USED>.*)$'),
XRegExp('^License\\s+Type:\\s+(?<LICENSE_TYPE>.+)'), XRegExp('^License\\s+Type:\\s*(?<LICENSE_TYPE>.*)$'),
XRegExp('^License\\s+State:\\s+(?<LICENSE_STATE>.+)'), XRegExp('^License\\s+State:\\s*(?<LICENSE_STATE>.*)$'),
XRegExp('^License\\s+Count:\\s+(?<LICENSE_COUNT>.+)'), XRegExp('^License\\s+Count:\\s*(?<LICENSE_COUNT>.*)$'),
XRegExp('^License\\s+Priority:\\s+(?<LICENSE_PRIORITY>.+)'), XRegExp('^License\\s+Priority:\\s*(?<LICENSE_PRIORITY>.*)$'),
] ]
const lines = data.split('\n') const lines = data.split('\n')
const records = [] const records: any[] = []
let currentRecord: any = null let currentRecord: any = null
for (const line of lines) { for (let line of lines) {
if (XRegExp.test(line, XRegExp('^Index\\s+\\d+\\s+Feature:'))) { line = line.trim() // ⭐ RẤT QUAN TRỌNG
// Start a new record
if (currentRecord) { if (!line) continue
records.push(currentRecord)
} const indexMatch = XRegExp.exec(line, patterns[0])
if (indexMatch) {
if (currentRecord) records.push(currentRecord)
currentRecord = { currentRecord = {
FEATURE: '', INDEX: indexMatch.groups?.INDEX || '',
FEATURE: indexMatch.groups?.FEATURE?.trim() || '',
PERIOD_LEFT: '', PERIOD_LEFT: '',
PERIOD_USED: '', PERIOD_USED: '',
LICENSE_TYPE: '', LICENSE_TYPE: '',
@ -33,28 +35,23 @@ const parseLog = (data: string) => {
LICENSE_COUNT: '', LICENSE_COUNT: '',
LICENSE_PRIORITY: '', LICENSE_PRIORITY: '',
} }
continue
} }
if (currentRecord) { if (!currentRecord) continue
for (const pattern of patterns) {
const match = XRegExp.exec(line, pattern) for (const p of patterns) {
if (match) { const m = XRegExp.exec(line, p)
const item = match?.groups || {} if (m?.groups) {
Object.keys(item).forEach((key) => { Object.entries(m.groups).forEach(([k, v]) => {
if (item && item[key] !== undefined) { currentRecord[k] = (v || '').trim()
currentRecord[key] = item[key] })
} break
})
break // Stop processing this line once a pattern matches
}
} }
} }
} }
// Push the last record if it exists if (currentRecord) records.push(currentRecord)
if (currentRecord) {
records.push(currentRecord)
}
return records return records
} }

1456
BACKEND/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,7 @@
"@swc/core": "1.11.24", "@swc/core": "1.11.24",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
"@types/nodemailer": "^7.0.4",
"eslint": "^9.26.0", "eslint": "^9.26.0",
"hot-hook": "^0.4.0", "hot-hook": "^0.4.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
@ -59,8 +60,11 @@
"@vinejs/vine": "^3.0.1", "@vinejs/vine": "^3.0.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"mysql2": "^3.15.3", "mysql2": "^3.15.3",
"net": "^1.0.2", "net": "^1.0.2",
"nodemailer": "^7.0.9",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"xregexp": "^5.1.2" "xregexp": "^5.1.2"

View File

@ -1,3 +1,5 @@
import moment from 'moment'
import momentTZ from 'moment-timezone'
import net from 'node:net' import net from 'node:net'
import fs from 'node:fs' import fs from 'node:fs'
import { Server as SocketIOServer } from 'socket.io' import { Server as SocketIOServer } from 'socket.io'
@ -9,7 +11,7 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
import Line from '#models/line' import Line from '#models/line'
import Station from '#models/station' import Station from '#models/station'
import APCController from '#services/apc_connection' import APCController from '#services/apc_connection'
import { appendLog, cleanData, sleep } from '../app/ultils/helper.js' import { appendLog, cleanData, sendMessageToMail, sleep } from '../app/ultils/helper.js'
import SwitchController from '#services/switch_connection' import SwitchController from '#services/switch_connection'
import redis from '@adonisjs/redis/services/main' import redis from '@adonisjs/redis/services/main'
import axios from 'axios' import axios from 'axios'
@ -541,17 +543,20 @@ export class WebSocketIo {
const results = await this.waitUntilAllReady(lineIds) const results = await this.waitUntilAllReady(lineIds)
const tableHTML = this.generateTable(results) const tableHTML = this.generateTable(results)
const d = new Date(Date.now()) const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
const dataFormat =
`${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}, ` +
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`
const linkWiki = const linkWiki =
process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test'
await axios.post(linkWiki, { await axios.post(linkWiki, {
data: tableHTML, data: tableHTML,
titleAuto: 'AUTO - ' + dataFormat, titleAuto: '[DPELP] Report AUTO - ' + dataFormat,
}) })
await sendMessageToMail(
'andrew.ng@apactech.io',
`[DPELP] Report AUTO - ${dataFormat}`,
tableHTML
)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -1059,19 +1064,20 @@ export class WebSocketIo {
generateTable(results: any) { generateTable(results: any) {
let html = `<table border="1" cellpadding="6" style="border-collapse: collapse; font-size: 14px;"> let html = `<table border="1" cellpadding="6" style="border-collapse: collapse; font-size: 14px;">
<tr> <tr>
<th style="text-align:center; width:65px;">Line</th> <th style="text-align:center; width:40px;">Line</th>
<th style="width:180px;">PID</th> <th style="width:190px;">PID</th>
<th style="text-align:center; width:140px;">SN</th> <th style="text-align:center; width:140px;">SN</th>
<th>MAC</th> <th>MAC</th>
<th>Mode</th> <th style="width:270px; text-align:center;">IOS</th>
<th style="width:600px; word-wrap: break-word; white-space: normal;">License</th> <th style="width:200px; word-wrap: break-word; white-space: normal;">License</th>
<th>Issues</th> <th>Issues</th>
</tr> </tr>
` `
for (const item of results) { for (const item of results) {
if (!item) continue
const licenses = Array.isArray(item.license) const licenses = Array.isArray(item.license)
? item.license ? [...new Set(item.license)]
: item.license : item.license
? [item.license] ? [item.license]
: [] : []
@ -1080,7 +1086,7 @@ export class WebSocketIo {
const licenseHTML = ` const licenseHTML = `
<div style=" <div style="
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px; gap: 4px;
width: 100%; width: 100%;
word-break: break-word; word-break: break-word;
@ -1091,12 +1097,12 @@ export class WebSocketIo {
html += ` html += `
<tr> <tr>
<td style="text-align:center;">${item.line}</td> <td style="text-align:center;">${item.line || ''}</td>
<td><div style="display:flex;"><div style="width:135px; margin-right:4px;">${item.pid}</div><div>| ${item.vid}</div></div></td> <td><div style="display:flex;"><div style="width:150px; margin-right:4px;">${item.pid || ''}</div><div> ${item.vid || ''}</div></div></td>
<td style="text-align:center;">${item.sn}</td> <td style="text-align:center;">${item.sn || ''}</td>
<td>${item.mac}</td> <td>${item.mac || ''}</td>
<td>${item.mode}</td> <td style="width:270px">${item.ios || ''}</td>
<td style="width:600px;">${licenseHTML}</td> <td style="width:200px;">${licenseHTML}</td>
<td>${item.issues?.length ? `- ` + item.issues.join(`<br>- `) : ''}</td> <td>${item.issues?.length ? `- ` + item.issues.join(`<br>- `) : ''}</td>
</tr> </tr>
` `