Update run scenario DPELP theo flow mới

This commit is contained in:
nguyentrungthat 2025-12-17 16:45:33 +07:00
parent 7528da2f00
commit 7c7778a0e1
13 changed files with 374 additions and 115 deletions

View File

@ -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()
)

View File

@ -29,6 +29,9 @@ export default class Scenario extends BaseModel {
@column()
declare send_result: boolean
@column()
declare sendResult: boolean
@column()
declare brandId: number

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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}
/>
<ModalConfirmRunScenario
socket={socket}
station={stations.find((el) => el.id === Number(activeTab))}
scenarios={scenarios}
/>
</Container>
);
}

View File

@ -237,7 +237,9 @@ const BottomToolBar = ({
justify={"center"}
h="100%"
>
<Text fz={"11px"}>Line {el.lineNumber}</Text>
<Text fz={"11px"}>
Line {el.lineNumber || el.line_number || ""}
</Text>
</Flex>
</Box>
))}
@ -507,7 +509,9 @@ const BottomToolBar = ({
}}
/>
<Flex align={"center"} justify={"center"} h="100%">
<Text fz={"10px"}>Line {el.lineNumber}</Text>
<Text fz={"10px"}>
Line {el.lineNumber || el.line_number || ""}
</Text>
</Flex>
</Box>
))}

View File

@ -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,

View File

@ -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<TLine[]>([]);
// const [listChecked, setListChecked] = useState<number[]>([]);
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 (
<Modal
style={{ position: "absolute", left: 0 }}
opened={opened}
closeOnClickOutside={false}
closeButtonProps={{ display: "none" }}
onClose={() => {
// close();
// setListChecked([]);
// setListLines([]);
}}
title={
<Text fz={"lg"} fw={"bolder"}>
Confirm run scenario
</Text>
}
size="xl"
>
<ScrollArea h={"60vh"} style={{ marginTop: "15px" }}>
<Table
stickyHeader
striped
highlightOnHover
withRowBorders={true}
withTableBorder={true}
withColumnBorders={true}
>
<Table.Thead
style={{
top: 1,
}}
>
<Table.Tr>
<Table.Th
style={{
width: "70px",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Line
</Table.Th>
<Table.Th
style={{
width: "140px",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Brand
</Table.Th>
<Table.Th
style={{
width: "140px ",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Category
</Table.Th>
<Table.Th
style={{
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Scenario
</Table.Th>
{/* <Table.Th
style={{
width: "70px",
textAlign: "center",
backgroundColor: "#94c6ff",
}}
>
Action
</Table.Th> */}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{listLines.current
?.sort((a, b) => (a?.id || 0) - (b?.id || 0))
?.map((line) => (
<Table.Tr key={line.id}>
<Table.Td
style={{
textAlign: "center",
}}
>
{line.lineNumber || line.line_number || ""}
</Table.Td>
<Table.Td
style={{
textAlign: "center",
}}
>
{line?.scenario?.brand?.name || ""}
</Table.Td>
<Table.Td
style={{
textAlign: "center",
}}
>
{line?.scenario?.category?.name || ""}
</Table.Td>
<Table.Td>
<Select
placeholder="Select scenario"
data={scenarios?.map((el) => ({
value: el.id.toString(),
label: el.title,
}))}
value={line.scenario?.id.toString() || null}
onChange={(value) => {
if (value)
listLines.current = listLines.current.map((li) =>
li.id === line.id
? {
...li,
scenario: scenarios.find(
(el) => el.id.toString() === value
),
}
: li
);
}}
/>
</Table.Td>
{/* <Table.Td
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "50px",
}}
>
<Checkbox
checked={listChecked?.includes(line?.id || 0)}
onChange={() => {
if (listChecked?.includes(line?.id || 0))
setListChecked((pre) =>
pre.filter((el) => el !== line?.id)
);
else setListChecked((pre) => [...pre, line?.id || 0]);
}}
/>
</Table.Td> */}
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
<Flex justify={"flex-end"}>
<Box>
<Button
color="green"
size="sm"
onClick={() => {
close();
listLines.current.forEach((line) => {
const scenario = line.scenario;
if (scenario)
socket?.emit(
"run_scenario",
Object.assign(line, {
scenario: {
...scenario,
isReboot:
typeof scenario?.isReboot !== "undefined"
? scenario?.isReboot
: scenario?.is_reboot,
},
})
);
});
listLines.current = [];
}}
>
Confirm
</Button>
</Box>
</Flex>
</Modal>
);
};
export default ModalConfirmRunScenario;

View File

@ -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,

View File

@ -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}
/>
</Grid.Col>
<Grid.Col span={2}>
@ -500,6 +528,8 @@ function ModalScenario({
onChange={(value) =>
form.setFieldValue("categoryId", value || "")
}
required
error={form.errors.categoryId}
/>
</Grid.Col>
<Grid.Col span={2}>

View File

@ -64,7 +64,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
</Box> */}
<TextInput
style={{ width: "300px" }}
value={element.expect}
value={element.expect || ""}
placeholder="Expect previous output"
onChange={(e) => {
const newBody = [...form.values.body];
@ -85,7 +85,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
<Table.Td>
<TextInput
style={{ width: "350px" }}
value={element.send}
value={element.send || ""}
placeholder="Command send"
onChange={(e) => {
const newBody = [...form.values.body];
@ -101,7 +101,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
<Table.Td>
<TextInput
style={{ width: "100px" }}
value={element.delay}
value={element.delay || ""}
placeholder="Delay send"
onChange={(e) => {
const value = numberOnly(e.target.value);
@ -120,7 +120,7 @@ const TableRows = ({ element, i, form, deleteRow, addRowUnder }: IPayload) => {
<Table.Td>
<TextInput
style={{ width: "70px" }}
value={element.repeat}
value={element.repeat || ""}
placeholder="Repeat"
onChange={(e) => {
const value = numberOnly(e.target.value);

View File

@ -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;