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('')
+ : `★ AI Potential 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'
+ ? ``
+ : ``
+ )
+ .join('')
+
+ // Physical Check photo placeholder cell (4 of these in the photo grid)
+ const photoCellHtml = (label: string) =>
+ ``
+
+ // ---- 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}
+ Type Switch — Layer 3
+ Cond. Refurb — Grade A
+ Supplier TechData AU — PO #TD-88432
+ Warranty 12 Months (→ May 2027)
+
+
+
+
+
+
+
+ Technical Specs
+
+
+ Specification
+ Actual
+ Default
+
+
+ IOS-XE Version
+ ${iosVersion}
+ 17.06.01
+
+
+ System RAM
+ ${memDisplay}
+ 8 GB
+
+
+ Flash Storage
+ ${flashDisplay}
+ 16 GB
+
+
+ Uplink Module
+ C9300-NM-4G
+ N/A
+
+
+ PSU Model
+ 715W AC
+ 715W AC
+
+
+ PoE Budget
+ 437 Watts
+ 437 Watts
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Issues Found
+ ${aiIssueRowsHtml}
+
+
+ COSMETIC Minor scratch on top chassis (2cm) — non-functional
+ Accepted
+
+
+
+
+ MINOR Fan #2 at 48dB under stress (spec 45dB) — within rack tolerance
+ Monitor
+
+
+ 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
+
+
+ Rackmount
+ PSU (Internal)
+ Console Cable
+ Documents
+ Original 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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-P LIT241525W1
+ C9300-NM-4G FDO2420H0X1
+ 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}
+
+
+
+
+
+
+ CONSOLE RAW OUTPUT (Boot Log snippet)
+ ${snapshot?.outputTestLog || 'No test log available'}
+
+
+
+
+
+
+ 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)
}
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`
-
+
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
+
+
+
+
@@ -2310,11 +2351,15 @@ Ports Missing/Down: ${missing.length}\n\n`
Hardware Inventory
- ${productPN} ${productSN}
- PWR-C1-715WAC-P LIT241525W1
- C9300-NM-4G FDO2420H0X1
- FAN-T2-GEN2 (x3) OK
-
+ ${
+ this.config?.inventory?.listInventory
+ ?.map(
+ (item: any) => `
+ ${item.pid} ${item.sn} `
+ )
+ .join('') || ''
+ }
+
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`
CONSOLE RAW OUTPUT (Boot Log snippet)
- ${snapshot?.outputTestLog || 'No test log available'}
+ ${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(() => {
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}
- Type Switch — Layer 3
- Cond. Refurb — Grade A
- Supplier TechData AU — PO #TD-88432
- Warranty 12 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`