From 95601a02cb406260d99a242fe161c27c32c5cff0 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Fri, 8 May 2026 16:41:32 +0700 Subject: [PATCH 1/3] Update --- BACKEND/app/services/line_connection.ts | 475 +++++++++++++++++++++++- BACKEND/app/ultils/helper.ts | 14 +- BACKEND/providers/socket_io_provider.ts | 22 +- 3 files changed, 492 insertions(+), 19 deletions(-) diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 78a39f9..1aad6c9 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -145,6 +145,7 @@ export default class LineConnection { private debounceSendSummaryReport: NodeJS.Timeout | null = null private isPingToServer: boolean private outputPingToServer: string + private outputTestLog: string constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { this.config = config @@ -180,6 +181,7 @@ export default class LineConnection { this.outputTestingPortPoE = '' this.isPingToServer = false this.outputPingToServer = '' + this.outputTestLog = '' } /** * Connect to line with socket @@ -225,6 +227,7 @@ export default class LineConnection { this.waitingScenario = true this.outputBuffer += message this.outputScenario += message + this.outputTestLog += message if (!this.config.inventory) this.outputInventory = this.outputInventory.slice(-3000) + message } @@ -1895,6 +1898,474 @@ Ports Missing/Down: ${missing.length}\n\n` }) } + /** + * Send summary report using the new "Equipment Receiving & Testing Report" template. + * Email-safe HTML: table-based layout, inline styles, no external CSS or web fonts. + */ + sendReportSummaryV2 = async (snapshot?: { + snapConfig: LineConfig + snapPhysical: PhysicalPortTest + reason: string + outputTestLog: string + }) => { + if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport) + + const physicalTest = snapshot?.snapPhysical ? snapshot?.snapPhysical : this.physicalTest + const config = snapshot?.snapConfig ? snapshot?.snapConfig : this.config + const portPhysical = Array.from(physicalTest.ports.values()) + const missing = portPhysical.filter((p) => !p.tested) + const missingPoE = missing.filter((p) => !p.name.includes('SFP')) + const missingSFP = missing.filter((p) => p.name.includes('SFP')) + const tested = portPhysical.filter((p) => p.tested) + const testedPoE = tested.filter((p) => !p.name.includes('SFP')) + const testedSFP = tested.filter((p) => p.name.includes('SFP')) + const totalPoE = testedPoE.length + missingPoE.length + const totalSFP = testedSFP.length + missingSFP.length + + const showVersion = config?.data?.find( + (d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver') + ) + const dataShowVersion = + showVersion?.textfsm && (showVersion?.textfsm as any)?.[0] + ? (showVersion?.textfsm as any)?.[0] + : config?.inventory + + const showLicense = config?.data?.find( + (d) => d.command?.trim()?.includes('show lic') || d.command?.trim()?.includes('sh lic') + ) + const dataShowLic = + showLicense?.textfsm && Array.isArray(showLicense?.textfsm) + ? (showLicense?.textfsm as any[]) + : null + + const issues: string[] = config?.latestScenario?.detectAI?.issue || [] + const skipReason = this.config.reasonSkipPhysical || snapshot?.reason || '' + const isSkipped = typeof skipReason === 'string' && skipReason.trim().length > 0 + + const verdictPass = missing.length === 0 && issues.length === 0 && !isSkipped + const verdictLabel = verdictPass ? 'PASSED' : 'NEEDS REVIEW' + const verdictMsg = verdictPass + ? 'All tests passed — Ready for deployment' + : 'Issues detected — review required before deployment' + const verdictBg = verdictPass ? '#ecfdf5' : '#fef2f2' + const verdictBd = verdictPass ? '#a7f3d0' : '#fecaca' + const verdictTx = verdictPass ? '#065f46' : '#991b1b' + + const reportId = `RPT-${config.stationId}-L${config.lineNumber}-${Date.now().toString().slice(-6)}` + const reportDate = momentTZ() + .tz(process.env.TIME_ZONE || 'UTC') + .format('DD MMM YYYY HH:mm') + + const memText = dataShowVersion?.MEMORY + ? convertFromKilobytesString(dataShowVersion.MEMORY) + : '—' + const flashText = dataShowVersion?.USB_FLASH + ? convertFromKilobytesString(dataShowVersion.USB_FLASH) + : '—' + + // ---- Template-fallback values (use file's hardcoded content when no real data) ---- + const productName = 'Cisco Catalyst 9300-48P-A' + const productPN = escapeHtml(String(config?.inventory?.pid || 'C9300-48P-A')) + const productSN = escapeHtml(String(config?.inventory?.sn || 'FCW2425L0KP')) + const productVid = escapeHtml(String(config?.inventory?.vid || 'V02')) + const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '17.09.04a')) + const memDisplay = escapeHtml(memText !== '—' ? memText : '8 GB') + const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '16 GB') + + // AI issue rows (one per real AI issue, fall back to file's hardcoded row when none) + const aiIssueRowsHtml = + issues.length > 0 + ? issues + .slice(0, 1) + .map( + (issue) => + `
★ AI${escapeHtml(issue)}Investigate
` + ) + .join('') + : `
★ AIPotential intermittent power instability. PSU #1 POST logs show 3 retries before handshake.Investigate
` + + // License boxes (real licenses if available, else file's hardcoded boxes) + const licenseBoxesHtml = + dataShowLic && dataShowLic.length > 0 + ? dataShowLic + .map( + (l: any) => + `
${escapeHtml(String(l.FEATURE || ''))}
${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}
` + ) + .join('') + : `
Network Advantage
Permanent · Smart License: Active
DNA Premier
Evaluation · 85 days remaining
` + + // Port stat values (real numbers if any port data, else file's defaults) + const hasPortData = portPhysical.length > 0 + const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '48/48' + const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '4/4' + const poeColor = + !hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b' + const sfpColor = + !hasPortData || (totalSFP > 0 && testedSFP.length === totalSFP) ? '#10b981' : '#f59e0b' + + // Missing-port detail blocks (only when there is something to show) + const missingParts: string[] = [] + if (missingPoE.length) { + missingParts.push( + `
Missing PoE (${missingPoE.length}):
${missingPoE.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}
` + ) + } + if (missingSFP.length) { + missingParts.push( + `
Missing SFP (${missingSFP.length}):
${missingSFP.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}
` + ) + } + if (isSkipped) { + missingParts.push( + `
User Skipped Physical Test:
${escapeHtml(skipReason)}
` + ) + } + const missingDetailsHtml = missingParts.join('') + + // Verdict checkmark / cross path + const verdictPathSvg = verdictPass + ? '' + : '' + + // Physical Check checklist + const checklistItems: Array<[string, string]> = [ + ['ok', 'Packaging intact — no damage to box or foam'], + ['ok', 'No physical damage — chassis, fans, PSU'], + ['ok', `S/N matches label — ${productSN} verified`], + ['ok', 'All 48 GigE + 4 SFP+ ports clean'], + ['ok', 'Accessories — power cable, rack ears, console cable'], + ['warn', 'Minor scratch on top chassis (2cm) — cosmetic only'], + ] + const checklistRowsHtml = checklistItems + .map(([k, t]) => + k === 'ok' + ? `
${t}
` + : `
!${t}
` + ) + .join('') + + // Physical Check photo placeholder cell (4 of these in the photo grid) + const photoCellHtml = (label: string) => + `
${label}
` + + // ---- Body: full template mirroring index.html, table-based + inline styles ---- + const body = ` + + + +Equipment Report — Mail Summary + + + + + + +
+ + +
+ + + + + +
+ + + + + +
+ + + PROLOGY IT + Equipment Receiving & Testing Report +
+
+ #${escapeHtml(reportId)} + ${escapeHtml(reportDate)} +
+
+ + +
+ ${verdictPathSvg} + ${verdictLabel} + ${escapeHtml(verdictMsg)} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + +
+
Product Info
+ + + + + + + + +
Name${productName}
P/N${productPN}
S/N${productSN}
TypeSwitch — Layer 3
Cond.Refurb — Grade A
SupplierTechData AU — PO #TD-88432
Warranty12 Months (→ May 2027)
+
+
+ + +
+
Technical Specs
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SpecificationActualDefault
IOS-XE Version${iosVersion}17.06.01
System RAM${memDisplay}8 GB
Flash Storage${flashDisplay}16 GB
Uplink ModuleC9300-NM-4GN/A
PSU Model715W AC715W AC
PoE Budget437 Watts437 Watts
+
+
+
+ + +
+
Issues Found
+ ${aiIssueRowsHtml} + + + + + +
COSMETICMinor scratch on top chassis (2cm) — non-functionalAccepted
+ + + + + +
MINORFan #2 at 48dB under stress (spec 45dB) — within rack toleranceMonitor
+
0 Critical · 0 Major · 1 Minor · 1 Cosmetic
+
+
+ + +
+
Receiving & Inspection Notes
+
+
⚠ Warning from Warehouse
+

Box arrived with slight indentation on the left corner. Internal foam was still intact. Serial number on box was partially obscured by shipping label but verified upon unboxing.

+
+
+
Accessory Checklist
+ + + + + + + + +
RackmountPSU (Internal)Console CableDocumentsOriginal Box
+
+
+
+ + +
+ + + + + + +
+
+
Received
+
Trung Nguyen
+
06 May 10:30
+
+
+
Physical Check
+
Khanh Le
+
06 May 11:15
+
+
+
Software Test
+
Duy Pham (remote)
+
06 May 14:00
+
+
+
+ + + + + + +
 Detail 
+
+ + +
+ + + + + +
+ + Physical Check + Khanh Le · 06 May 11:15
+ + + + + +
+ + + + + + + + + +
${photoCellHtml('Front')}${photoCellHtml('Rear')}
${photoCellHtml('S/N Label')}${photoCellHtml('Package')}
+
+ ${checklistRowsHtml} +
+
+
+ + +
+ + + + + +
+ + Software Check + Duy Pham (remote) · 06 May 14:00–17:45
+ + + + + + +
+
Hardware Inventory
+ + + + + +
${productPN}${productSN}
PWR-C1-715WAC-PLIT241525W1
C9300-NM-4GFDO2420H0X1
FAN-T2-GEN2 (x3)OK
+
+
System & License
+ ${licenseBoxesHtml} +
+
Port Test Summary
+ + + + + + + + + +
${escapeHtml(poeText)}
${hasPortData ? 'PoE UP' : 'GigE UP'}
${escapeHtml(sfpText)}
SFP+ UP
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
+ ${missingDetailsHtml} +
+ + + + +
${snapshot?.outputTestLog || 'No test log available'}
+
CONSOLE RAW OUTPUT (Boot Log snippet)
+
+
Prology IT — Equipment QA System · Confidential — Internal Use Only
+ + +` + + // this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP) + await sendMessageToMail( + `[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Summary of Testing Results`, + body + ) + this.socketIO.emit('summary_tested', { + stationId: this.config.stationId, + lineId: this.config.id, + body: body, + title: `[${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`, + }) + } + /** * Reset config information of line */ @@ -1933,6 +2404,7 @@ Ports Missing/Down: ${missing.length}\n\n` snapConfig: this.config, snapPhysical: this.physicalTest, reason: '', + outputTestLog: this.outputTestLog, } this.debounceSendSummaryReport = setTimeout(() => { if (!this.config.listFeatureTested?.includes('PHYSICAL')) { @@ -1942,7 +2414,8 @@ Ports Missing/Down: ${missing.length}\n\n` } this.config.listFeatureTested = ['DPELP', 'PHYSICAL', 'SUMMARY'] this.sendFeatureTested() - this.sendReportSummary(snapshot) + this.sendReportSummaryV2(snapshot) + this.outputTestLog = '' }, timeout) } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 39bbfe3..685ed1d 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -11,13 +11,13 @@ import ConfigRam from '#models/config_ram' import Keyword from '#models/keywords' const mailTo = 'andrew.ng@apactech.io' -const mailCC = [ - 'ips@ipsupply.com.au', - 'kay@ipsupply.com.au', - 'joseph@apactech.io', - 'kiet.phan@apactech.io', -] -// const mailCC = '' +// const mailCC = [ +// 'ips@ipsupply.com.au', +// 'kay@ipsupply.com.au', +// 'joseph@apactech.io', +// 'kiet.phan@apactech.io', +// ] +const mailCC = '' type DetectAI = { status: string[] diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index ac70af9..89c936d 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -678,10 +678,10 @@ export class WebSocketIo { const linkWiki = process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' try { - await axios.post(linkWiki, { - data: tableHTML, - titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, - }) + // await axios.post(linkWiki, { + // data: tableHTML, + // titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, + // }) } catch (error) { console.error('Error sending wiki message:', error) } @@ -697,13 +697,13 @@ export class WebSocketIo { const contentZulip = `\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` + zulipMess - await sendMessageToZulip( - 'stream', - streamZulip || 'ATC_Report', - topicZulip, - contentZulip - ) - await sendMessageToZulip('stream', 'ATC_Report', station.name, contentZulip) + // await sendMessageToZulip( + // 'stream', + // streamZulip || 'ATC_Report', + // topicZulip, + // contentZulip + // ) + // await sendMessageToZulip('stream', 'ATC_Report', station.name, contentZulip) } catch (error) { console.error('Error sending zulip message:', error) } -- 2.39.2 From 7ea6af3c28e6a63d12baade618c249cdd46cac77 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Sat, 9 May 2026 10:34:37 +0700 Subject: [PATCH 2/3] Update user login, form --- BACKEND/app/controllers/auth_controller.ts | 27 +++- BACKEND/app/models/user.ts | 6 + BACKEND/app/services/line_connection.ts | 131 ++++++++++++------ ...add_first_name_last_name_to_users_table.ts | 19 +++ BACKEND/providers/socket_io_provider.ts | 4 +- .../Modal/ModalConfirmRunPhysicalTest.tsx | 21 ++- 6 files changed, 153 insertions(+), 55 deletions(-) create mode 100644 BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts diff --git a/BACKEND/app/controllers/auth_controller.ts b/BACKEND/app/controllers/auth_controller.ts index b423cc5..22cabea 100644 --- a/BACKEND/app/controllers/auth_controller.ts +++ b/BACKEND/app/controllers/auth_controller.ts @@ -6,7 +6,7 @@ export default class AuthController { // Đăng ký async register({ request, response }: HttpContext) { try { - const data = request.only(['email', 'password', 'user_name']) + const data = request.only(['email', 'password', 'user_name', 'first_name', 'last_name']) const user = await User.query().where('user_name', data.user_name).first() @@ -23,7 +23,12 @@ export default class AuthController { // Đăng nhập async login({ request, auth, response }: HttpContext) { - const { user_name: userName, password } = request.only(['user_name', 'password']) + const { user_name: userName, password } = request.only([ + 'user_name', + 'password', + 'first_name', + 'last_name', + ]) const user = await User.query().where('user_name', userName).first() if (!user) { @@ -47,10 +52,18 @@ export default class AuthController { email: remoteUser.userEmail, userName: userName, password: password, + firstName: remoteUser?.firstName || null, + lastName: remoteUser?.lastName || null, }) return response.json({ message: 'Login successful', - user: { id: newUser.id, email: newUser.email, userName: newUser.userName }, + user: { + id: newUser.id, + email: newUser.email, + userName: newUser.userName, + firstName: newUser.firstName, + lastName: newUser.lastName, + }, }) } @@ -62,7 +75,13 @@ export default class AuthController { return response.json({ message: 'Login successful', - user: { id: user.id, email: user.email, userName: user.userName }, + user: { + id: user.id, + email: user.email, + userName: user.userName, + firstName: user.firstName, + lastName: user.lastName, + }, }) } catch { return response.status(401).json({ message: 'Invalid credentials' }) diff --git a/BACKEND/app/models/user.ts b/BACKEND/app/models/user.ts index e5068e2..ce897bf 100644 --- a/BACKEND/app/models/user.ts +++ b/BACKEND/app/models/user.ts @@ -14,6 +14,12 @@ export default class User extends BaseModel { @column() declare password: string + @column() + declare firstName: string | null + + @column() + declare lastName: string | null + @column.dateTime({ autoCreate: true }) declare createdAt: DateTime diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 1aad6c9..ed5011a 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -545,10 +545,11 @@ export default class LineConnection { if ( ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command) ) { - const dataInventory = JSON.parse(item.textfsm)[0] + const listInventory = JSON.parse(item.textfsm) + const dataInventory = listInventory[0] this.config.inventory = this.config.inventory - ? { ...this.config.inventory, ...dataInventory } - : dataInventory + ? { ...this.config.inventory, ...dataInventory, listInventory } + : { ...dataInventory, listInventory } pid = dataInventory?.pid || '' this.addHistory(this.config.stationId, this.config.id, { id: this.config.id, @@ -807,9 +808,10 @@ export default class LineConnection { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) { - const dataInventory = JSON.parse(item.textfsm)[0] + const listInventory = JSON.parse(item.textfsm) + const dataInventory = listInventory[0] this.config.inventory = this.config.inventory - ? { ...this.config.inventory, ...dataInventory } + ? { ...this.config.inventory, ...dataInventory, listInventory } : dataInventory } item.textfsm = JSON.parse(item.textfsm) @@ -1164,7 +1166,7 @@ Ports Missing/Down: ${missing.length}\n\n` /** * Starting physical test (PoE ports testing) */ - async runPhysicalTest() { + async runPhysicalTest(userName?: string) { if (this.config.runningPhysical) { console.log('Running physical test') return @@ -1964,13 +1966,13 @@ Ports Missing/Down: ${missing.length}\n\n` : '—' // ---- Template-fallback values (use file's hardcoded content when no real data) ---- - const productName = 'Cisco Catalyst 9300-48P-A' - const productPN = escapeHtml(String(config?.inventory?.pid || 'C9300-48P-A')) - const productSN = escapeHtml(String(config?.inventory?.sn || 'FCW2425L0KP')) - const productVid = escapeHtml(String(config?.inventory?.vid || 'V02')) - const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '17.09.04a')) - const memDisplay = escapeHtml(memText !== '—' ? memText : '8 GB') - const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '16 GB') + const productName = escapeHtml(String(config?.inventory?.name || '')) + const productPN = escapeHtml(String(config?.inventory?.pid || '')) + const productSN = escapeHtml(String(config?.inventory?.sn || '')) + const productVid = escapeHtml(String(config?.inventory?.vid || '')) + const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '')) + const memDisplay = escapeHtml(memText !== '—' ? memText : '') + const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '') // AI issue rows (one per real AI issue, fall back to file's hardcoded row when none) const aiIssueRowsHtml = @@ -1988,6 +1990,7 @@ Ports Missing/Down: ${missing.length}\n\n` const licenseBoxesHtml = dataShowLic && dataShowLic.length > 0 ? dataShowLic + .filter((l) => l.LICENSE_TYPE && l.FEATURE) .map( (l: any) => `
${escapeHtml(String(l.FEATURE || ''))}
${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}
` @@ -1997,8 +2000,8 @@ Ports Missing/Down: ${missing.length}\n\n` // Port stat values (real numbers if any port data, else file's defaults) const hasPortData = portPhysical.length > 0 - const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '48/48' - const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '4/4' + const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '0/0' + const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '0/0' const poeColor = !hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b' const sfpColor = @@ -2104,7 +2107,7 @@ Ports Missing/Down: ${missing.length}\n\n` @@ -2310,11 +2351,15 @@ Ports Missing/Down: ${missing.length}\n\n` ` + ) + .join('') || '' + } +
- +
Product Info
@@ -2220,27 +2223,65 @@ Ports Missing/Down: ${missing.length}\n\n`
+ + + + + + + +
+ + - - - + -
-
-
Received
-
Trung Nguyen
-
06 May 10:30
-
-
-
Physical Check
-
Khanh Le
-
06 May 11:15
-
-
-
Software Test
-
Duy Pham (remote)
-
06 May 14:00
-
+
+   +
+
+ +
+
+ ✓ +
+
+ Received +
+
+ Trung Nguyen +
+
+ 06 May 10:30 +
+
+
+ ✓ +
+ +
+ Physical Check +
+
+ Khanh Le +
+
+ 06 May 11:15 +
+
+
+ ✓ +
+
+ Software Test +
+
+ Duy Pham (remote) +
+
+ 06 May 14:00 +
+
Hardware Inventory
- - - - -
${productPN}${productSN}
PWR-C1-715WAC-PLIT241525W1
C9300-NM-4GFDO2420H0X1
FAN-T2-GEN2 (x3)OK
+ ${ + this.config?.inventory?.listInventory + ?.map( + (item: any) => ` +
${item.pid}${item.sn}
System & License
@@ -2328,8 +2373,8 @@ Ports Missing/Down: ${missing.length}\n\n`
${escapeHtml(sfpText)}
SFP+ UP
-
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
-
${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
+
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
+
${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
${missingDetailsHtml} @@ -2340,7 +2385,7 @@ Ports Missing/Down: ${missing.length}\n\n` -
${snapshot?.outputTestLog || 'No test log available'}
+
CONSOLE RAW OUTPUT (Boot Log snippet)
${snapshot?.outputTestLog || 'No test log available'}
diff --git a/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts new file mode 100644 index 0000000..800c368 --- /dev/null +++ b/BACKEND/database/migrations/1778550000000_add_first_name_last_name_to_users_table.ts @@ -0,0 +1,19 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'users' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.string('first_name').nullable().after('user_name') + table.string('last_name').nullable().after('first_name') + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('first_name') + table.dropColumn('last_name') + }) + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 89c936d..b540061 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -728,7 +728,7 @@ export class WebSocketIo { }) socket.on('run_physical_test', async (data) => { - const { stationId, lineId } = data + const { stationId, lineId, userName: name } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return @@ -737,7 +737,7 @@ export class WebSocketIo { io, stationId, [lineId], - async (lineCon) => lineCon.runPhysicalTest(), + async (lineCon) => lineCon.runPhysicalTest(name), {} ) }) diff --git a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx index ffb7a69..6bb5f6d 100644 --- a/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx +++ b/FRONTEND/src/components/Modal/ModalConfirmRunPhysicalTest.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Modal, Box, @@ -33,6 +33,12 @@ export default function ModalConfirmRunPhysical({ socket, station, }: Props) { + const user = useMemo(() => { + return localStorage.getItem("user") && + typeof localStorage.getItem("user") === "string" + ? JSON.parse(localStorage.getItem("user") || "") + : null; + }, []); const [dataLines, setDataLines] = useState([]); const [isDisabled, setIsDisabled] = useState(false); @@ -48,7 +54,7 @@ export default function ModalConfirmRunPhysical({ sn: line?.inventory?.sn, vid: line?.inventory?.vid, checked: true, - })) + })), ); } }, [listLines]); @@ -100,8 +106,8 @@ export default function ModalConfirmRunPhysical({ ...el, checked: e.target.checked, } - : el - ) + : el, + ), ) } /> @@ -133,13 +139,16 @@ export default function ModalConfirmRunPhysical({ socket?.emit("run_physical_test", { lineId: line?.id, stationId: Number(station?.id), + userName: user?.firstName + ? `${user.firstName} ${user.lastName || ""}` + : user?.userName || "Unknown User", }); }); setDataLines(listNotChecked); setListLines( listLines.filter((el) => - listNotChecked.find((li) => li.id === el.id) - ) + listNotChecked.find((li) => li.id === el.id), + ), ); setIsDisabled(true); setTimeout(() => { -- 2.39.2 From 36a67c5b2d0c361ce5d4138e71f014a3f56000a7 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 11 May 2026 16:25:15 +0700 Subject: [PATCH 3/3] Update form summary --- BACKEND/app/services/line_connection.ts | 123 ++++++++++-------- BACKEND/app/ultils/helper.ts | 14 +- BACKEND/app/ultils/templates/show_version.ts | 6 +- BACKEND/providers/socket_io_provider.ts | 29 +++-- FRONTEND/src/components/BottomToolBar.tsx | 23 ++-- FRONTEND/src/components/ButtonAction.tsx | 20 ++- FRONTEND/src/components/CardLine.tsx | 10 ++ .../src/components/Modal/ModalTerminal.tsx | 36 ++--- 8 files changed, 153 insertions(+), 108 deletions(-) diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index ed5011a..a442953 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -146,6 +146,10 @@ export default class LineConnection { private isPingToServer: boolean private outputPingToServer: string private outputTestLog: string + private userTest: { + dpelp: { name: string; time: number } + physical: { name: string; time: number } + } constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { this.config = config @@ -182,6 +186,7 @@ export default class LineConnection { this.isPingToServer = false this.outputPingToServer = '' this.outputTestLog = '' + this.userTest = { dpelp: { name: '', time: 0 }, physical: { name: '', time: 0 } } } /** * Connect to line with socket @@ -227,7 +232,7 @@ export default class LineConnection { this.waitingScenario = true this.outputBuffer += message this.outputScenario += message - this.outputTestLog += message + this.outputTestLog += cleanData(data.toString()) if (!this.config.inventory) this.outputInventory = this.outputInventory.slice(-3000) + message } @@ -442,6 +447,10 @@ export default class LineConnection { }) if (script?.send_result || script?.sendResult) { this.dataDPELP = '' + this.userTest = { + ...this.userTest, + dpelp: { name: userName || '', time: Date.now() }, + } // this.config.inventory = '' } @@ -1178,6 +1187,7 @@ Ports Missing/Down: ${missing.length}\n\n` this.config.reasonSkipPhysical = '' this.testingPortPoE = true this.outputTestingPortPoE = '' + this.userTest = { ...this.userTest, physical: { name: userName || '', time: Date.now() } } const listPorts = await this.getPorts() this.socketIO.emit('running_scenario', { stationId: this.config.stationId, @@ -1909,9 +1919,13 @@ Ports Missing/Down: ${missing.length}\n\n` snapPhysical: PhysicalPortTest reason: string outputTestLog: string + userTest: { + dpelp: { name: string; time: number } + physical: { name: string; time: number } + } }) => { if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport) - + const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const physicalTest = snapshot?.snapPhysical ? snapshot?.snapPhysical : this.physicalTest const config = snapshot?.snapConfig ? snapshot?.snapConfig : this.config const portPhysical = Array.from(physicalTest.ports.values()) @@ -1944,7 +1958,7 @@ Ports Missing/Down: ${missing.length}\n\n` const skipReason = this.config.reasonSkipPhysical || snapshot?.reason || '' const isSkipped = typeof skipReason === 'string' && skipReason.trim().length > 0 - const verdictPass = missing.length === 0 && issues.length === 0 && !isSkipped + const verdictPass = missing.length === 0 && !isSkipped const verdictLabel = verdictPass ? 'PASSED' : 'NEEDS REVIEW' const verdictMsg = verdictPass ? 'All tests passed — Ready for deployment' @@ -1953,10 +1967,8 @@ Ports Missing/Down: ${missing.length}\n\n` const verdictBd = verdictPass ? '#a7f3d0' : '#fecaca' const verdictTx = verdictPass ? '#065f46' : '#991b1b' - const reportId = `RPT-${config.stationId}-L${config.lineNumber}-${Date.now().toString().slice(-6)}` - const reportDate = momentTZ() - .tz(process.env.TIME_ZONE || 'UTC') - .format('DD MMM YYYY HH:mm') + const reportId = `RPT-${momentTZ().tz(timeZone).format('YYYY-MMDD')}` + const reportDate = momentTZ().tz(timeZone).format('DD MMM YYYY') const memText = dataShowVersion?.MEMORY ? convertFromKilobytesString(dataShowVersion.MEMORY) @@ -1971,8 +1983,10 @@ Ports Missing/Down: ${missing.length}\n\n` const productSN = escapeHtml(String(config?.inventory?.sn || '')) const productVid = escapeHtml(String(config?.inventory?.vid || '')) const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || '')) - const memDisplay = escapeHtml(memText !== '—' ? memText : '') - const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '') + const macAddress = escapeHtml(String(dataShowVersion?.MAC_ADDRESS || '')) + const memDisplay = escapeHtml(memText !== '—' ? memText : '-') + const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '-') + const configRam = await detectConfigRamByModel(config?.inventory?.pid) // AI issue rows (one per real AI issue, fall back to file's hardcoded row when none) const aiIssueRowsHtml = @@ -1996,7 +2010,7 @@ Ports Missing/Down: ${missing.length}\n\n` `
${escapeHtml(String(l.FEATURE || ''))}
${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}
` ) .join('') - : `
Network Advantage
Permanent · Smart License: Active
DNA Premier
Evaluation · 85 days remaining
` + : `` // Port stat values (real numbers if any port data, else file's defaults) const hasPortData = portPhysical.length > 0 @@ -2111,13 +2125,14 @@ Ports Missing/Down: ${missing.length}\n\n`
Product Info
- - - - - - - + + + + + + + +
Name${productName}
P/N${productPN}
S/N${productSN}
TypeSwitch — Layer 3
Cond.Refurb — Grade A
SupplierTechData AU — PO #TD-88432
Warranty12 Months (→ May 2027)
Name${productName}
P/N${productPN}
S/N${productSN}
MAC${macAddress}
Type--
Cond.--
Supplier-
Warranty-
@@ -2135,32 +2150,32 @@ Ports Missing/Down: ${missing.length}\n\n` IOS-XE Version ${iosVersion} - 17.06.01 + - System RAM ${memDisplay} - 8 GB + ${configRam?.ram || '-'} Flash Storage ${flashDisplay} - 16 GB + ${configRam?.flash || '-'} Uplink Module - C9300-NM-4G - N/A + - + - PSU Model - 715W AC - 715W AC + - + - PoE Budget - 437 Watts - 437 Watts + - + - @@ -2227,7 +2242,7 @@ Ports Missing/Down: ${missing.length}\n\n` -
+
 
@@ -2243,13 +2258,28 @@ Ports Missing/Down: ${missing.length}\n\n` Received
- Trung Nguyen + Unknown
-
- 06 May 10:30 +
+ ${momentTZ().tz(timeZone).format('DD MMM')}
+ +
+ ✓ +
+
+ Software Test +
+
+ ${snapshot?.userTest?.dpelp?.name || ''} +
+
+ ${momentTZ(snapshot?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')} +
+ +
✓ @@ -2259,25 +2289,10 @@ Ports Missing/Down: ${missing.length}\n\n` Physical Check
- Khanh Le + ${snapshot?.userTest?.physical?.name || ''}
-
- 06 May 11:15 -
- - - -
- ✓ -
-
- Software Test -
-
- Duy Pham (remote) -
-
- 06 May 14:00 +
+ ${momentTZ(snapshot?.userTest?.physical?.time).tz(timeZone).format('DD MMM, HH:mm')}
@@ -2307,7 +2322,7 @@ Ports Missing/Down: ${missing.length}\n\n` Physical Check - Khanh Le · 06 May 11:15 + ${snapshot?.userTest?.physical?.name || ''} · ${momentTZ(snapshot?.userTest?.physical?.time).tz(timeZone).format('DD MMM, HH:mm')} @@ -2343,7 +2358,7 @@ Ports Missing/Down: ${missing.length}\n\n` Software Check - +
Duy Pham (remote) · 06 May 14:00–17:45${snapshot?.userTest?.dpelp?.name || ''} · ${momentTZ(snapshot?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')}
@@ -2373,8 +2388,8 @@ Ports Missing/Down: ${missing.length}\n\n` - - + +
${escapeHtml(sfpText)}
SFP+ UP
${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}
PoE+ Test
${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
${missingSFP.length > 0 || missingPoE.length > 0 ? 'WARN' : 'PASS'}
PoE+ Test
${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%
Throughput
${missingDetailsHtml} @@ -2398,7 +2413,7 @@ Ports Missing/Down: ${missing.length}\n\n` ` - // this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP) + this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP) await sendMessageToMail( `[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Summary of Testing Results`, body @@ -2450,6 +2465,7 @@ Ports Missing/Down: ${missing.length}\n\n` snapPhysical: this.physicalTest, reason: '', outputTestLog: this.outputTestLog, + userTest: this.userTest, } this.debounceSendSummaryReport = setTimeout(() => { if (!this.config.listFeatureTested?.includes('PHYSICAL')) { @@ -2461,6 +2477,7 @@ Ports Missing/Down: ${missing.length}\n\n` this.sendFeatureTested() this.sendReportSummaryV2(snapshot) this.outputTestLog = '' + this.userTest = { dpelp: { name: '', time: 0 }, physical: { name: '', time: 0 } } }, timeout) } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 685ed1d..39bbfe3 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -11,13 +11,13 @@ import ConfigRam from '#models/config_ram' import Keyword from '#models/keywords' const mailTo = 'andrew.ng@apactech.io' -// const mailCC = [ -// 'ips@ipsupply.com.au', -// 'kay@ipsupply.com.au', -// 'joseph@apactech.io', -// 'kiet.phan@apactech.io', -// ] -const mailCC = '' +const mailCC = [ + 'ips@ipsupply.com.au', + 'kay@ipsupply.com.au', + 'joseph@apactech.io', + 'kiet.phan@apactech.io', +] +// const mailCC = '' type DetectAI = { status: string[] diff --git a/BACKEND/app/ultils/templates/show_version.ts b/BACKEND/app/ultils/templates/show_version.ts index a72d19e..556e5c9 100644 --- a/BACKEND/app/ultils/templates/show_version.ts +++ b/BACKEND/app/ultils/templates/show_version.ts @@ -3,9 +3,11 @@ import XRegExp from 'xregexp' // Parser function const parseLog = (data: string) => { const patterns = [ - XRegExp('^.*Software.*\\((?\\S+)\\),\\s+Version\\s+(?[\\w\\.-]+)'), XRegExp( - '^\\*?\\s*\\d+\\s+\\d+\\s+[\\w-]+\\s+(?\\d[\\w\\.-]+)\\s+(?[\\w-]+)\\s+(?:BUNDLE|INSTALL)' + '^.*Software.*\\((?\\S+)\\),\\s+Version\\s+(?[\\w\\.\\(\\)\\-]+)' + ), + XRegExp( + '^\\*?\\s*\\d+\\s+\\d+\\s+[\\w-]+\\s+(?\\d[\\w\\.\\(\\)\\-]+)\\s+(?[\\w-]+)\\s+(?:BUNDLE|INSTALL)' ), XRegExp('System\\s+image\\s+file\\s+is\\s+"(?:[^:]*:)?(?[^"]+)"'), XRegExp('Active-image:\\s+(?\\S+)'), diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index b540061..f8a3df5 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -203,6 +203,7 @@ export class WebSocketIo { socket.on('run_scenario', async (data) => { const lineId = data.id const scenario = data.scenario + const name = data.userName || userName // Check station is active const activeStation = await checkStationActive(data.stationId) if (!activeStation) return @@ -211,7 +212,7 @@ export class WebSocketIo { io, data.stationId, [lineId], - async (line) => line.runScript(scenario, userName), + async (line) => line.runScript(scenario, name), { scenario, } @@ -678,10 +679,10 @@ export class WebSocketIo { const linkWiki = process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' try { - // await axios.post(linkWiki, { - // data: tableHTML, - // titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, - // }) + await axios.post(linkWiki, { + data: tableHTML, + titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, + }) } catch (error) { console.error('Error sending wiki message:', error) } @@ -697,13 +698,13 @@ export class WebSocketIo { const contentZulip = `\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` + zulipMess - // await sendMessageToZulip( - // 'stream', - // streamZulip || 'ATC_Report', - // topicZulip, - // contentZulip - // ) - // await sendMessageToZulip('stream', 'ATC_Report', station.name, contentZulip) + await sendMessageToZulip( + 'stream', + streamZulip || 'ATC_Report', + topicZulip, + contentZulip + ) + await sendMessageToZulip('stream', 'ATC_Report', station.name, contentZulip) } catch (error) { console.error('Error sending zulip message:', error) } @@ -737,7 +738,7 @@ export class WebSocketIo { io, stationId, [lineId], - async (lineCon) => lineCon.runPhysicalTest(name), + async (lineCon) => lineCon.runPhysicalTest(name || userName), {} ) }) @@ -907,7 +908,7 @@ export class WebSocketIo { stationId, [lineId], async (lineCon) => { - lineCon.sendReportSummary() + lineCon.sendReportSummaryV2() }, {} ) diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index 61032c3..c5da472 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -223,8 +223,8 @@ const BottomToolBar = ({ onClick={() => { setSelectedLines( selectedLines.filter( - (line) => line.id !== el.id - ) + (line) => line.id !== el.id, + ), ); socket?.emit("close_cli", { lineId: el?.id, @@ -288,7 +288,7 @@ const BottomToolBar = ({ const lines = station.lines.filter( (line) => !line?.userOpenCLI || - line?.userOpenCLI === user?.userName + line?.userOpenCLI === user?.userName, ); if (selectedLines.length !== lines.length) { setSelectedLines(lines); @@ -365,7 +365,7 @@ const BottomToolBar = ({ selectedLines={selectedLines} isDisable={isDisable || selectedLines.length === 0} dataDPELP={scenarios?.find( - (el) => el.title.toUpperCase() === "DPELP" + (el) => el.title.toUpperCase() === "DPELP", )} onClick={() => { if (selectedLines.length > 0) { @@ -380,6 +380,11 @@ const BottomToolBar = ({ setIsDisable(false); }, 5000); }} + userName={ + user?.firstName + ? `${user.firstName} ${user.lastName || ""}` + : user?.userName || "Unknown User" + } />