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