Updaet wiki
This commit is contained in:
parent
ae01929bf2
commit
8cf801d964
|
|
@ -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 và xuất kết quả theo đúng format:
|
Hãy phân tích đoạn log sau và 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 2–4 ý)
|
|
||||||
|
|
||||||
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 có lỗi → ghi rõ "No issues detected.".
|
Nếu log không có lỗi → ghi rõ "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}
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
`
|
`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue