diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 7863d78..db74622 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -438,7 +438,7 @@ export default class LineConnection { }) this.outputBuffer = '' this.outputScenario = '' - this.config.output += 'Timeout run scenario' + this.config.output += '\nTimeout run scenario\n' this.dataDPELP = { line: this.config.lineNumber, pid: '', @@ -453,7 +453,7 @@ export default class LineConnection { this.socketIO.emit('line_output', { stationId: this.config.stationId, lineId: this.config.id, - data: 'Timeout run scenario', + data: '\nTimeout run scenario\n', }) this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n` appendLog( @@ -970,7 +970,7 @@ export default class LineConnection { // console.log(detectLog) const tableHTML = this.buildEmailContent(result) await sendMessageToMail( - `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue`, + `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue ${result?.errors?.some((e) => e.category === 'SPECIAL_KEYWORD') ? '+ Special keywords' : ''}`, tableHTML + `${`
@@ -1012,7 +1012,7 @@ export default class LineConnection { ${r.rule} ${r.message} - *${escapeHtml(r.log.trim()) + ${escapeHtml(r.log.trim()) .split('*') .filter((el) => el) .join('
*')} diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 9b78346..6d27c65 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -8,6 +8,7 @@ import axios from 'axios' import moment from 'moment' import Station from '#models/station' import ConfigRam from '#models/config_ram' +import Keyword from '#models/keywords' const mailTo = 'andrew.ng@apactech.io' const mailCC = [ @@ -36,7 +37,10 @@ type InputData = { // Types type SendMailResponse = string type SendMessageType = 'stream' | 'private' - +type KeywordMatchType = 'contains' | 'exact' +interface KeywordRule extends LogRule { + keywordId?: number +} /** * Function to clean up unwanted characters from the output data. * @param {string} data - The raw data to be cleaned. @@ -393,7 +397,7 @@ export function classifyLog(line: string): ParsedLog { if (/ERROR|FAIL|CRITICAL|Traceback/.test(line)) return { raw: line, category: 'ERROR' } - return { raw: line, category: 'UNKNOWN' } + return { raw: line, category: 'SPECIAL_KEYWORD' } } export const RULES: LogRule[] = [ @@ -537,32 +541,36 @@ export const RULES: LogRule[] = [ }, ] -export function applyRules(log: ParsedLog): TestError[] { - return RULES.filter( - (rule): rule is LogRule & { level: 'FAIL' | 'WARN' } => - rule.category === log.category && rule.match.test(log.raw) && rule.level !== 'PASS' - ).map((rule) => ({ - ruleId: rule.id, - level: rule.level, // ✅ giờ TS biết chắc chỉ FAIL | WARN - message: rule.message, - evidence: { - raw: log.raw, - timestamp: log.timestamp, - }, - })) +export async function applyRules(log: ParsedLog): Promise { + const KEYWORD_RULES: KeywordRule[] = await loadKeywordRules(log.raw) + return [...RULES, ...KEYWORD_RULES] + .filter( + (rule): rule is LogRule & { level: 'FAIL' | 'WARN' } => + rule.category === log.category && rule.match.test(log.raw) && rule.level !== 'PASS' + ) + .map((rule) => ({ + ruleId: rule.id, + level: rule.level, // ✅ giờ TS biết chắc chỉ FAIL | WARN + message: rule.message, + category: rule.category, + evidence: { + raw: log.raw, + timestamp: log.timestamp, + }, + })) } export class TestSession { bootOk = false errors: TestError[] = [] - applyParsedLog(log: ParsedLog) { + async applyParsedLog(log: ParsedLog) { // Detect boot OK if (/IOS XE Software|System Bootstrap/.test(log.raw)) { this.bootOk = true } - const matchedErrors = applyRules(log) + const matchedErrors = await applyRules(log) matchedErrors.forEach((err) => this.addError(err)) } @@ -595,7 +603,7 @@ export class TestSession { const hasWarn = this.errors.some((e) => e.level === 'WARN') let status: TestResult['status'] = 'PASS' - if (!this.bootOk || hasFail) status = 'FAIL' + if (hasFail) status = 'FAIL' else if (hasWarn) status = 'PARTIAL' return { @@ -1476,3 +1484,40 @@ export function convertToKilobytes(input: string): number { return Math.round(value * unitMultipliers[unit]) } + +function escapeRegex(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function keywordToRule(keyword: Keyword): KeywordRule { + let match: RegExp + + switch (keyword.match_type) { + case 'exact': + match = new RegExp(`^${escapeRegex(keyword.name)}$`, 'i') + break + + case 'contains': + default: + match = new RegExp(escapeRegex(keyword.name), 'i') + } + + return { + id: `${keyword.name}`, + keywordId: keyword.id, + category: 'SPECIAL_KEYWORD', + match, + level: 'WARN', + message: `Keyword detected: ${keyword.name}`, + } +} + +async function loadKeywordRules(log: string): Promise { + const keywords = await Keyword.query() + for (const keyword of keywords) { + if (log.toUpperCase().includes(keyword.name.toUpperCase())) { + return keywords.map(keywordToRule) + } + } + return [] +} diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts index d25ebe5..7031d10 100644 --- a/BACKEND/app/ultils/types.ts +++ b/BACKEND/app/ultils/types.ts @@ -8,7 +8,14 @@ export interface CustomServer extends Server { userKeys?: string[] } -type LogCategory = 'BOOT' | 'LICENSE' | 'INTERFACE' | 'HARDWARE' | 'ERROR' | 'UNKNOWN' +type LogCategory = + | 'BOOT' + | 'LICENSE' + | 'INTERFACE' + | 'HARDWARE' + | 'ERROR' + | 'UNKNOWN' + | 'SPECIAL_KEYWORD' export interface ParsedLog { raw: string @@ -35,6 +42,7 @@ export interface TestError { ruleId: string level: 'FAIL' | 'WARN' message: string + category: string evidence: { raw: string timestamp?: Date diff --git a/FRONTEND/src/components/Modal/ModalKeywords.tsx b/FRONTEND/src/components/Modal/ModalKeywords.tsx index 75ab6b6..66f4a6c 100644 --- a/FRONTEND/src/components/Modal/ModalKeywords.tsx +++ b/FRONTEND/src/components/Modal/ModalKeywords.tsx @@ -24,7 +24,7 @@ const LIST_TYPE = [ ]; const LIST_MATCH_TYPE = [ { value: "exact", label: "Exact" }, - { value: "include", label: "Include" }, + { value: "contains", label: "Contains" }, ]; interface Props { opened: boolean; @@ -37,7 +37,7 @@ export default function ModalKeywords({ opened, onClose }: Props) { const [newKeywords, setNewKeywords] = useState({ name: "", type: "special_model", - match_type: "include", + match_type: "contains", is_active: true, }); const [inputModel, setInputModel] = useState(""); @@ -64,7 +64,7 @@ export default function ModalKeywords({ opened, onClose }: Props) { setNewKeywords({ name: "", type: "special_model", - match_type: "include", + match_type: "contains", is_active: true, }); } @@ -104,7 +104,7 @@ export default function ModalKeywords({ opened, onClose }: Props) { setNewKeywords({ name: "", type: "special_model", - match_type: "include", + match_type: "contains", is_active: true, }); setDisabled(false);