From 4e5099aea8963ee2ffe7846d0595225ff9c82613 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:30:17 +0700 Subject: [PATCH] Update send zulip --- BACKEND/app/models/scenario.ts | 3 + BACKEND/app/models/station.ts | 3 + BACKEND/app/services/line_connection.ts | 32 ++-- BACKEND/app/services/switch_connection.ts | 13 +- BACKEND/app/ultils/helper.ts | 52 +++++++ ...5032433_add_is_active_to_stations_table.ts | 17 +++ ...1882_add_send_result_to_scenarios_table.ts | 17 +++ BACKEND/package-lock.json | 138 +++++++++++++++++- BACKEND/package.json | 3 +- BACKEND/providers/socket_io_provider.ts | 79 +++++++++- FRONTEND/src/components/InputHistory.tsx | 1 + 11 files changed, 333 insertions(+), 25 deletions(-) create mode 100644 BACKEND/database/migrations/1765155032433_add_is_active_to_stations_table.ts create mode 100644 BACKEND/database/migrations/1765162501882_add_send_result_to_scenarios_table.ts diff --git a/BACKEND/app/models/scenario.ts b/BACKEND/app/models/scenario.ts index d050bbb..1dbfcab 100644 --- a/BACKEND/app/models/scenario.ts +++ b/BACKEND/app/models/scenario.ts @@ -22,4 +22,7 @@ export default class Scenario extends BaseModel { @column.dateTime({ autoCreate: true, autoUpdate: true }) declare updatedAt: DateTime + + @column() + declare send_result: boolean } diff --git a/BACKEND/app/models/station.ts b/BACKEND/app/models/station.ts index 986661e..a9c104a 100644 --- a/BACKEND/app/models/station.ts +++ b/BACKEND/app/models/station.ts @@ -82,4 +82,7 @@ export default class Station extends BaseModel { @column() declare send_wiki: boolean + + @column() + declare is_active: boolean } diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index c1eff0d..109beef 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -165,7 +165,7 @@ export default class LineConnection { if (!this.config.inventory) this.outputInventory = this.outputInventory.slice(-3000) + message } - if (message.includes('--More--')) this.writeCommand(' ') + if (data.toString().includes('--More--')) this.writeCommand(' ') // let output = cleanData(message) // console.log(`📨 [${this.config.port}] ${message}`) @@ -735,19 +735,23 @@ export default class LineConnection { messages: [ { role: 'user', - 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: - Yêu cầu đầu ra đúng cấu trúc: - issue: - (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: - Không giải thích dài dò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.". - - Ngắn gọn, dễ đọc, đúng format - Return only json format with English. + content: `You are a network hardware tester. + Your task is to analyze router/switch logs to determine whether the device meets hardware standards for reselling. + Focus ONLY on hardware-related problems or abnormal warnings. + Software or configuration issues (e.g., port up/down, admin down, invalid commands, CLI errors, licensing messages) should be ignored unless they indicate hardware failure. + OUTPUT FORMAT (must follow exactly): + { + "issue": [ "problem 1", "problem 2", ... ], + "summary": "short summary under 30 words" + } + RULES: + - Summaries must be in English. + - Each issue must be one short line. + - If the log contains no hardware issues, output: { "issue": ["No issues detected."], "summary": "No hardware issues found." } + - Keep responses concise, readable, and strictly in JSON format. + - Do NOT add explanations outside the JSON. + - Your job is to detect hardware faults, missing components, overheating, failing modules, PSU issues, sensor anomalies, SIM/card missing, modem errors, transceiver issues, POST/diagnostics failures, etc. + The log to analyze will be provided after this prompt. Here is the log: diff --git a/BACKEND/app/services/switch_connection.ts b/BACKEND/app/services/switch_connection.ts index 7e8b335..c6120a3 100644 --- a/BACKEND/app/services/switch_connection.ts +++ b/BACKEND/app/services/switch_connection.ts @@ -73,13 +73,13 @@ export default class SwitchController { this.isEnable = false this.onData(this.portGroups, this.status) if (this.retryConnect <= 5) { - await this.sleep(15000) - console.log('Retry connect times', this.retryConnect) - this.retryConnect += 1 - await this.reconnect() - } else { this.retryConnect = 0 + return } + await this.sleep(15000) + console.log('SWITCH Retry connect times', this.retryConnect) + this.retryConnect += 1 + await this.reconnect() } private _handleError(err: Error & { code?: string }) { @@ -145,7 +145,8 @@ export default class SwitchController { } public disconnect() { - this.socket.end() + // this.socket.end() + this.socket.destroy() this.status = 'DISCONNECTED' this.isEnable = false } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 38e8c7d..8769a2a 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import nodeMailer from 'nodemailer' +import zulip from 'zulip-js' type DetectAI = { status: string[] @@ -18,6 +19,7 @@ type InputData = { // Types type SendMailResponse = string +type SendMessageType = 'stream' | 'private' /** * Function to clean up unwanted characters from the output data. @@ -247,3 +249,53 @@ export function sendMessageToMail( }) }) } + +export function sendMessageToZulip( + type: SendMessageType, + to: string | number | string[], + topic: string | undefined, + content: string +): Promise | null { + return new Promise((resolve, reject) => { + const config = { + realm: process.env.ZULIP_REALM as string, + username: process.env.ZULIP_USERNAME as string, + apiKey: process.env.ZULIP_API_KEY as string, + } + + zulip(config).then((client: any) => { + if (type === 'stream') { + client.messages + .send({ + type, + to, + topic: topic || '', + content, + }) + .then((response: any) => { + console.log('Message sent: ' + JSON.stringify(response)) + resolve(response) + }) + .catch((error: any) => { + console.error(error) + reject(error) + }) + } else if (type === 'private') { + client.messages + .send({ + type, + to, + content, + }) + .then((response: any) => { + console.log('Message sent: ' + JSON.stringify(response)) + resolve(response) + }) + .catch((error: any) => { + console.error(error) + reject(error) + }) + } + }) + }) +} diff --git a/BACKEND/database/migrations/1765155032433_add_is_active_to_stations_table.ts b/BACKEND/database/migrations/1765155032433_add_is_active_to_stations_table.ts new file mode 100644 index 0000000..294a633 --- /dev/null +++ b/BACKEND/database/migrations/1765155032433_add_is_active_to_stations_table.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'stations' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.boolean('is_active').defaultTo(true) + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('is_active') + }) + } +} diff --git a/BACKEND/database/migrations/1765162501882_add_send_result_to_scenarios_table.ts b/BACKEND/database/migrations/1765162501882_add_send_result_to_scenarios_table.ts new file mode 100644 index 0000000..d4be040 --- /dev/null +++ b/BACKEND/database/migrations/1765162501882_add_send_result_to_scenarios_table.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'scenarios' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.boolean('send_result').defaultTo(false) + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('send_result') + }) + } +} diff --git a/BACKEND/package-lock.json b/BACKEND/package-lock.json index b18b9eb..bc55978 100644 --- a/BACKEND/package-lock.json +++ b/BACKEND/package-lock.json @@ -24,7 +24,8 @@ "nodemailer": "^7.0.9", "reflect-metadata": "^0.2.2", "socket.io": "^4.8.1", - "xregexp": "^5.1.2" + "xregexp": "^5.1.2", + "zulip-js": "^2.1.0" }, "devDependencies": { "@adonisjs/assembler": "^7.8.2", @@ -1417,6 +1418,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime-corejs3": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", @@ -6149,6 +6159,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -6320,6 +6339,63 @@ "devOptional": true, "license": "ISC" }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-form-data": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz", + "integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==", + "license": "MIT", + "dependencies": { + "form-data": "^2.3.2" + } + }, + "node_modules/isomorphic-form-data/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/isomorphic-form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/isomorphic-form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/jest-diff": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", @@ -6964,6 +7040,26 @@ "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -8724,6 +8820,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/truncatise": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/truncatise/-/truncatise-0.0.8.tgz", @@ -9025,6 +9127,28 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9265,6 +9389,18 @@ "engines": { "node": ">= 0.6" } + }, + "node_modules/zulip-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zulip-js/-/zulip-js-2.1.0.tgz", + "integrity": "sha512-kLdxzJZ/FvWHBotUJl7LXCHIkShTjy1FUk5HAWfsal1TM+hw0atCZwgasCpvFDBj01y+39ZEZXgjePaie74Xhg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "isomorphic-form-data": "2.0.0" + } } } } diff --git a/BACKEND/package.json b/BACKEND/package.json index b3b74b4..6af2047 100644 --- a/BACKEND/package.json +++ b/BACKEND/package.json @@ -67,7 +67,8 @@ "nodemailer": "^7.0.9", "reflect-metadata": "^0.2.2", "socket.io": "^4.8.1", - "xregexp": "^5.1.2" + "xregexp": "^5.1.2", + "zulip-js": "^2.1.0" }, "hotHook": { "boundaries": [ diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 82efbb6..af011f9 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -11,7 +11,13 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js' import Line from '#models/line' import Station from '#models/station' import APCController from '#services/apc_connection' -import { appendLog, cleanData, sendMessageToMail, sleep } from '../app/ultils/helper.js' +import { + appendLog, + cleanData, + sendMessageToMail, + sendMessageToZulip, + sleep, +} from '../app/ultils/helper.js' import SwitchController from '#services/switch_connection' import redis from '@adonisjs/redis/services/main' import axios from 'axios' @@ -554,6 +560,7 @@ export class WebSocketIo { const results = await this.waitUntilAllReady(lineIds) const tableHTML = this.generateTable(results) + const zulipMess = this.generateZulipMessage(results) const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss') @@ -562,14 +569,20 @@ export class WebSocketIo { process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' await axios.post(linkWiki, { data: tableHTML, - titleAuto: `[DPELP] Report AUTO - ${stationName} - ` + dataFormat, + titleAuto: `[DPELP] - ${stationName} - ` + dataFormat, }) await sendMessageToMail( 'andrew.ng@apactech.io', - `[DPELP] Report AUTO - ${stationName} - ${dataFormat}`, + `[DPELP] - ${stationName} - ${dataFormat}`, tableHTML, ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] ) + await sendMessageToZulip( + 'stream', + 'ATC_Report', + station.name, + `\n\n---\n[DPELP] - ${stationName} - ${dataFormat}\n` + zulipMess + ) } catch (error) { console.log(error) } @@ -655,6 +668,10 @@ export class WebSocketIo { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] } + if (this.intervalKeepConnect[`${lineId}`]) { + clearInterval(this.intervalKeepConnect[`${lineId}`]) + delete this.intervalKeepConnect[`${lineId}`] + } }, timeout) this.intervalMap[`${lineId}`] = interval @@ -1067,6 +1084,44 @@ export class WebSocketIo { return html } + /** + * Generates a Zulip-compatible Markdown table string from the results array. + * Uses
to force line breaks within the License cell. + * @param {Array} results - The array of data objects. + * @returns {string} The Markdown table string. + */ + generateZulipMessage(results: any[]) { + let msg = '' + + for (const item of results) { + if (!item) continue + + const licenses = Array.isArray(item.license) + ? [...new Set(item.license)] + : item.license + ? [item.license] + : [] + + const issues = item.issues || [] + + msg += `**Line ${item.line || '?'} — ${item.pid || ''}${item.vid ? ` (${item.vid})` : ''} ` + msg += `**SN:** **${item.sn || ''}** \n` + msg += '```' + `\n` + + msg += `MAC: ${item.mac || ''} \n` + msg += `IOS: ${item.ios || ''} \n` + + msg += `License: ${licenses.join(', ') || ''} \n` + msg += `Issues: \n` + if (issues.length) { + for (const i of issues) msg += `• ${i} \n` + } else msg += `• No issues detected.\n` + msg += '```\n' + } + + return msg + } + private async connectStation(station: Station) { try { const stationConn = new StationConnection({ @@ -1081,6 +1136,7 @@ export class WebSocketIo { await stationConn.connect() stationConn.writeCommand('\r\n') this.setTimeoutConnect(station.id, stationConn) + this.keepConnectStation(station.id) } catch (error) { console.log(error) } @@ -1120,4 +1176,21 @@ export class WebSocketIo { console.log('Station connect error:', err.message) } } + + private keepConnectStation = (id: number) => { + if (this.intervalKeepConnect[`${id}`]) { + clearInterval(this.intervalKeepConnect[`${id}`]) + delete this.intervalKeepConnect[`${id}`] + } + const interval = setInterval(async () => { + const station = this.stationMap.get(id) + if (station) { + await this.handleStationOperation(id, async (stationCon) => { + stationCon.writeCommand(`\r\n`) + }) + } + }, 120000) + + this.intervalKeepConnect[`${id}`] = interval + } } diff --git a/FRONTEND/src/components/InputHistory.tsx b/FRONTEND/src/components/InputHistory.tsx index ab03ec6..292554a 100644 --- a/FRONTEND/src/components/InputHistory.tsx +++ b/FRONTEND/src/components/InputHistory.tsx @@ -111,6 +111,7 @@ export default function InputHistory({ return (