diff --git a/BACKEND/app/controllers/scenarios_controller.ts b/BACKEND/app/controllers/scenarios_controller.ts index 4b3e805..27a6c0b 100644 --- a/BACKEND/app/controllers/scenarios_controller.ts +++ b/BACKEND/app/controllers/scenarios_controller.ts @@ -135,6 +135,7 @@ export default class ScenariosController { const existedScenarios = await Scenario.query().select('id', 'series') const duplicatedSeries: string[] = [] for (const sc of existedScenarios) { + if (sc.id === scenarioId) continue const scSeries: string[] = JSON.parse(sc.series || '[]').map((s: string) => s.trim().toUpperCase() ) diff --git a/BACKEND/app/models/scenario.ts b/BACKEND/app/models/scenario.ts index c3a6c03..8ff2ecb 100644 --- a/BACKEND/app/models/scenario.ts +++ b/BACKEND/app/models/scenario.ts @@ -29,6 +29,9 @@ export default class Scenario extends BaseModel { @column() declare send_result: boolean + @column() + declare sendResult: boolean + @column() declare brandId: number diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 6747f0d..6a2efe1 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -1,7 +1,14 @@ import fs from 'node:fs' import { textfsmResults } from './../ultils/templates/index.js' import net from 'node:net' -import { appendLog, cleanData, isValidJson, mapToLineFormat, sleep } from '../ultils/helper.js' +import { + appendLog, + cleanData, + detectScenarioByModel, + isValidJson, + mapToLineFormat, + sleep, +} from '../ultils/helper.js' import Scenario from '#models/scenario' import Station from '#models/station' import APCController from './apc_connection.js' @@ -345,7 +352,8 @@ export default class LineConnection { lineId: this.config.id, title: script?.title, }) - if (script?.send_result) this.dataDPELP = '' + if (script?.send_result || script?.sendResult) this.dataDPELP = '' + if (script?.isReboot) { await sleep(10000) for (let index = 0; index < 30; index++) { @@ -381,6 +389,17 @@ export default class LineConnection { this.outputBuffer = '' this.outputScenario = '' this.config.output += 'Timeout run scenario' + this.dataDPELP = { + line: this.config.lineNumber, + pid: '', + vid: '', + sn: '', + ios: '', + mac: '', + license: [], + issues: ['No data'], + summary: '', + } this.socketIO.emit('line_output', { stationId: this.config.stationId, lineId: this.config.id, @@ -426,6 +445,7 @@ export default class LineConnection { const logScenarios = this.outputScenario const data = textfsmResults(logScenarios, '') + let pid = '' try { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { @@ -434,6 +454,7 @@ export default class LineConnection { ) { const dataInventory = JSON.parse(item.textfsm)[0] this.config.inventory = dataInventory + pid = dataInventory?.pid || '' this.addHistory(this.config.stationId, this.config.id, { id: this.config.id, number: this.config.lineNumber, @@ -448,6 +469,18 @@ export default class LineConnection { item.textfsm = JSON.parse(item.textfsm) } }) + const scenario = await detectScenarioByModel(pid) + // console.log(pid, scenario) + if (scenario && scenario.id !== script.id) { + this.outputScenario = '' + // this.runScript(scenario, userName) + this.socketIO.emit('confirm_scenario', { + scenario: scenario, + id: this.config.id, + }) + resolve(true) + return + } const detectLog = await this.detectLogWithAI(logScenarios) const result = mapToLineFormat({ lineNumber: this.config.lineNumber, @@ -457,7 +490,7 @@ export default class LineConnection { }, data, }) - if (script?.send_result) { + if (script?.send_result || script?.sendResult) { this.dataDPELP = result console.log( `DPELP DATA line ${this.config.lineNumber} of ${this.config.stationName}:`, @@ -589,102 +622,14 @@ export default class LineConnection { return false } - async apcControl(action: 'on' | 'off' | 'restart') { - try { - const station = await Station.find(this.config.stationId) - if (!station) throw new Error('Station not found') - - const apcName = this.config.apcName || 'apc_1' - const ip = (station as any)[`${apcName}_ip`] as string - const port = (station as any)[`${apcName}_port`] as number - const username = (station as any)[`${apcName}_username`] as string - const password = (station as any)[`${apcName}_password`] as string - - if (!ip || !port || !username || !password) - throw new Error(`Missing APC configuration for ${apcName}`) - - // Tạo APC Controller instance - const apc = new APCController({ - host: ip, - port, - username, - password, - stationId: this.config.stationId, - stationName: this.config.stationName, - stationIP: this.config.stationIp, - number: this.config.lineNumber, - onData: (data: string, status: string) => { - this.config.output += data - this.socketIO.emit('apc_output', { - stationId: this.config.stationId, - lineId: this.config.id, - apcNumber: apcName === 'apc_1' ? 1 : 2, - data, - status, - }) - appendLog( - cleanData(data), - this.config.stationId, - this.config.stationName, - this.config.stationIp, - this.config.lineNumber - ) - }, - }) - - // Connect và login - await apc.connect() - await apc.login() - - // Thực thi hành động - this.socketIO.emit('apc_status', { - stationId: this.config.stationId, - lineId: this.config.id, - action, - status: 'running', - }) - - switch (action) { - case 'on': - await apc.turnOnOutlet(this.config.outlet) - break - case 'off': - await apc.turnOffOutlet(this.config.outlet) - break - case 'restart': - await apc.restartOutlet(this.config.outlet) - break - } - - // Hoàn thành - this.socketIO.emit('apc_status', { - stationId: this.config.stationId, - lineId: this.config.id, - action, - status: 'done', - }) - - apc.disconnect() - } catch (error) { - const msg = (error as Error).message - console.error('APC Control error:', msg) - this.socketIO.emit('apc_status', { - stationId: this.config.stationId, - lineId: this.config.id, - action, - status: 'error', - message: msg, - }) - } - } - getInventory = () => { const data = textfsmResults(this.outputInventory, 'show inventory') try { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) { - this.config.inventory = JSON.parse(item.textfsm)[0] + const dataInventory = JSON.parse(item.textfsm)[0] + this.config.inventory = dataInventory } item.textfsm = JSON.parse(item.textfsm) } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 8393a2b..59d5140 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -1,3 +1,4 @@ +import Scenario from '#models/scenario' import fs from 'node:fs' import path from 'node:path' import nodeMailer from 'nodemailer' @@ -304,3 +305,30 @@ export function sendMessageToZulip( }) }) } + +// Catch scenario with key longer +export const detectScenarioByModel = async (model: string) => { + let scenarios = await Scenario.query().preload('brand').preload('category') + const normalizedModel = model.trim().toUpperCase() + let matched: { scenario: Scenario; score: number } | null = null + + for (const scenario of scenarios) { + const seriesList: string[] = Array.isArray(scenario.series) + ? scenario.series + : JSON.parse(scenario.series || '[]') + + for (const s of seriesList) { + const pattern = s.trim().toUpperCase() + + if (normalizedModel.startsWith(pattern)) { + const score = pattern.length + + if (!matched || score > matched.score) { + matched = { scenario, score } + } + } + } + } + + return matched?.scenario || null +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 767f4e0..be8eb5f 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -22,6 +22,7 @@ import SwitchController from '#services/switch_connection' import redis from '@adonisjs/redis/services/main' import axios from 'axios' import StationConnection from '#services/station_connection' +import Scenario from '#models/scenario' interface HandleOptions { command?: string @@ -573,23 +574,24 @@ export class WebSocketIo { const linkWiki = process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' - await axios.post(linkWiki, { - data: tableHTML, - titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, - }) + // await axios.post(linkWiki, { + // data: tableHTML, + // 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'] - ) - await sendMessageToZulip( - 'stream', - 'ATC_Report', - station.name, - `\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` + - zulipMess + tableHTML + // , + // ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] ) + // await sendMessageToZulip( + // 'stream', + // 'ATC_Report', + // station.name, + // `\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` + + // zulipMess + // ) } catch (error) { console.log(error) } diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index c5886ca..5193669 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -52,6 +52,7 @@ import PageLogin from "./components/Authentication/LoginPage"; import DraggableTabs from "./components/DragTabs"; import { isJsonString } from "./untils/helper"; import BottomToolBar from "./components/BottomToolBar"; +import ModalConfirmRunScenario from "./components/Modal/ModalConfirmRunScenario"; const apiUrl = import.meta.env.VITE_BACKEND_URL; @@ -825,6 +826,12 @@ function App() { stationItem={stations.find((el) => el.id === Number(activeTab))} scenarios={scenarios} /> + + el.id === Number(activeTab))} + scenarios={scenarios} + /> ); } diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index a4e72b9..eec3aa5 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -237,7 +237,9 @@ const BottomToolBar = ({ justify={"center"} h="100%" > - Line {el.lineNumber} + + Line {el.lineNumber || el.line_number || ""} + ))} @@ -507,7 +509,9 @@ const BottomToolBar = ({ }} /> - Line {el.lineNumber} + + Line {el.lineNumber || el.line_number || ""} + ))} diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index 63d5158..d50d56f 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -228,7 +228,7 @@ export const ButtonScenario = ({ apcName: "apc_2", }); } - if (scenario?.send_result) + if (scenario?.send_result || scenario?.sendResult) socket?.emit("run_all_dpelp", { lineIds: selectedLines?.map((el) => el.id), stationName: station?.name, diff --git a/FRONTEND/src/components/Modal/ModalConfirmRunScenario.tsx b/FRONTEND/src/components/Modal/ModalConfirmRunScenario.tsx new file mode 100644 index 0000000..007c539 --- /dev/null +++ b/FRONTEND/src/components/Modal/ModalConfirmRunScenario.tsx @@ -0,0 +1,234 @@ +import { + Box, + Button, + Flex, + Modal, + ScrollArea, + Select, + Table, + Text, +} from "@mantine/core"; +import type { Socket } from "socket.io-client"; +import { useEffect, useRef } from "react"; +import type { IScenario, TLine, TStation } from "../../untils/types"; +import { useDisclosure } from "@mantine/hooks"; + +const ModalConfirmRunScenario = ({ + socket, + station, + scenarios, +}: { + socket: Socket | null; + station: TStation | undefined; + scenarios: IScenario[]; +}) => { + const [opened, { open, close }] = useDisclosure(false); + const listLines = useRef([]); + // const [listChecked, setListChecked] = useState([]); + + useEffect(() => { + if (!socket) return; + socket.on("confirm_scenario", (data) => { + const line = station?.lines?.find((el) => el.id === data.id); + console.log(data, line); + if (listLines.current.find((el) => el.id === data.id)) return; + listLines.current = [ + ...listLines.current, + line ? { ...line, ...data } : data, + ]; + // setListChecked([...listChecked, data.id]); + if (!opened) open(); + }); + // ✅ cleanup on unmount or when socket changes + return () => { + socket.off("confirm_scenario"); + }; + }, [socket, listLines, opened, station]); + + return ( + { + // close(); + // setListChecked([]); + // setListLines([]); + }} + title={ + + Confirm run scenario + + } + size="xl" + > + + + + + + Line + + + Brand + + + Category + + + Scenario + + {/* + Action + */} + + + + {listLines.current + ?.sort((a, b) => (a?.id || 0) - (b?.id || 0)) + ?.map((line) => ( + + + {line.lineNumber || line.line_number || ""} + + + {line?.scenario?.brand?.name || ""} + + + {line?.scenario?.category?.name || ""} + + +
+
+ + + + + +
+ ); +}; + +export default ModalConfirmRunScenario; diff --git a/FRONTEND/src/components/Modal/ModalRunScenario.tsx b/FRONTEND/src/components/Modal/ModalRunScenario.tsx index 2d65add..b6b1389 100644 --- a/FRONTEND/src/components/Modal/ModalRunScenario.tsx +++ b/FRONTEND/src/components/Modal/ModalRunScenario.tsx @@ -491,7 +491,10 @@ const ModalRunScenario = ({ apcName: "apc_2", }); } - if (scenario?.send_result) + if ( + scenario?.send_result || + scenario?.sendResult + ) socket?.emit("run_all_dpelp", { lineIds: selectedLines?.map((el) => el.id), stationName: station.name, diff --git a/FRONTEND/src/components/Modal/ModalScenario.tsx b/FRONTEND/src/components/Modal/ModalScenario.tsx index e982cfb..bde43bc 100644 --- a/FRONTEND/src/components/Modal/ModalScenario.tsx +++ b/FRONTEND/src/components/Modal/ModalScenario.tsx @@ -94,6 +94,14 @@ function ModalScenario({ if (!value) return "Title is required"; return null; }, + brandId: (value) => { + if (!value) return "Brand is required"; + return null; + }, + categoryId: (value) => { + if (!value) return "Category is required"; + return null; + }, }, }); @@ -131,6 +139,22 @@ function ModalScenario({ }); return; } + if (!form.values.brandId) { + notifications.show({ + title: "Error", + message: "Brand is required", + color: "red", + }); + return; + } + if (!form.values.categoryId) { + notifications.show({ + title: "Error", + message: "Category is required", + color: "red", + }); + return; + } setIsSubmit(true); try { const body = form.values.body.map((el: IBodyScenario) => ({ @@ -345,7 +369,9 @@ function ModalScenario({ ); form.setFieldValue( "send_result", - scenario.send_result + typeof scenario.sendResult !== "undefined" + ? scenario.sendResult + : scenario?.send_result ); form.setFieldValue("note", scenario.note); form.setFieldValue( @@ -486,6 +512,8 @@ function ModalScenario({ onChange={(value) => form.setFieldValue("brandId", value || "") } + required + error={form.errors.brandId} /> @@ -500,6 +528,8 @@ function ModalScenario({ onChange={(value) => form.setFieldValue("categoryId", value || "") } + required + error={form.errors.categoryId} /> diff --git a/FRONTEND/src/components/Modal/Scenario/TableRows.tsx b/FRONTEND/src/components/Modal/Scenario/TableRows.tsx index a325a1b..a6946a6 100644 --- a/FRONTEND/src/components/Modal/Scenario/TableRows.tsx +++ b/FRONTEND/src/components/Modal/Scenario/TableRows.tsx @@ -64,7 +64,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => { */} { const newBody = [...form.values.body]; @@ -85,7 +85,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => { { const newBody = [...form.values.body]; @@ -101,7 +101,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => { { const value = numberOnly(e.target.value); @@ -120,7 +120,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => { { const value = numberOnly(e.target.value); diff --git a/FRONTEND/src/untils/types.ts b/FRONTEND/src/untils/types.ts index d24ed94..90ed465 100644 --- a/FRONTEND/src/untils/types.ts +++ b/FRONTEND/src/untils/types.ts @@ -103,6 +103,7 @@ export type TLine = { tickets?: TDataTicket[]; connecting?: boolean; runningScenario?: string; + scenario?: IScenario; }; export type TUser = { @@ -165,6 +166,7 @@ export type IScenario = { isReboot: boolean; is_reboot: boolean; send_result: boolean; + sendResult?: boolean; brandId: number; brand_id?: number; brand?: TBrands;