This commit is contained in:
nguyentrungthat 2026-05-08 16:41:32 +07:00
parent 5a79a0448e
commit 95601a02cb
3 changed files with 492 additions and 19 deletions

View File

@ -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) =>
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">&#9733; AI</span><span style="margin-left:8px;vertical-align:middle;">${escapeHtml(issue)}</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
)
.join('')
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">&#9733; AI</span><span style="margin-left:8px;vertical-align:middle;">Potential intermittent power instability. PSU #1 POST logs show 3 retries before handshake.</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
// License boxes (real licenses if available, else file's hardcoded boxes)
const licenseBoxesHtml =
dataShowLic && dataShowLic.length > 0
? dataShowLic
.map(
(l: any) =>
`<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">${escapeHtml(String(l.FEATURE || ''))}</div><div style="font-size:10px;color:#9ca3af;">${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}</div></div>`
)
.join('')
: `<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">Network Advantage</div><div style="font-size:10px;color:#9ca3af;">Permanent · Smart License: Active</div></div><div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">DNA Premier</div><div style="font-size:10px;color:#9ca3af;">Evaluation · 85 days remaining</div></div>`
// 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(
`<div style="margin-top:8px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing PoE (${missingPoE.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingPoE.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
)
}
if (missingSFP.length) {
missingParts.push(
`<div style="margin-top:6px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing SFP (${missingSFP.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingSFP.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
)
}
if (isSkipped) {
missingParts.push(
`<div style="margin-top:6px;padding:8px 12px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:10px;color:#92400e;"><b>User Skipped Physical Test:</b><br/>${escapeHtml(skipReason)}</div>`
)
}
const missingDetailsHtml = missingParts.join('')
// Verdict checkmark / cross path
const verdictPathSvg = verdictPass
? '<path d="M6.5 10l2.5 2.5 4.5-4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>'
: '<path d="M7 7l6 6M13 7l-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>'
// 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'
? `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#ecfdf5;border:1px solid #a7f3d0;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#065f46;"><span style="display:inline-block;width:18px;height:18px;background:#a7f3d0;color:#065f46;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">&#10003;</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#92400e;"><span style="display:inline-block;width:18px;height:18px;background:#fde68a;color:#92400e;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">!</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
)
.join('')
// Physical Check photo placeholder cell (4 of these in the photo grid)
const photoCellHtml = (label: string) =>
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="border:1px dashed #e5e7eb;border-radius:6px;background:#f9fafb;border-collapse:separate;"><tr><td align="center" style="padding:18px 0;color:#9ca3af;"><svg viewBox="0 0 40 40" width="22" height="22" fill="none" style="display:inline-block;color:#9ca3af;"><rect x="4" y="8" width="32" height="24" rx="3" stroke="currentColor" stroke-width="1.5"/><circle cx="14" cy="18" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M4 28l8-6 6 4 8-8 10 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><div style="font-size:9px;font-weight:600;margin-top:3px;">${label}</div></td></tr></table>`
// ---- Body: full template mirroring index.html, table-based + inline styles ----
const body = `<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<title>Equipment Report Mail Summary</title>
</head>
<body style="margin:0;padding:24px 16px 48px;background:#f3f4f6;color:#1a1d23;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.5;">
<!-- HEADER + VERDICT -->
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto 12px;">
<tr><td>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-bottom:none;border-radius:10px 10px 0 0;border-collapse:separate;">
<tr><td style="padding:14px 20px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="vertical-align:middle;">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="background:#1e293b;border-radius:8px;width:34px;height:34px;text-align:center;vertical-align:middle;color:#cbd5e1;padding:6px;">
<svg viewBox="0 0 32 32" width="22" height="22" fill="none" style="display:block;margin:auto;color:#cbd5e1;"><rect x="2" y="6" width="28" height="20" rx="3" stroke="currentColor" stroke-width="2"/><path d="M8 16h16M8 12h10M8 20h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="24" cy="20" r="2" fill="currentColor"/></svg>
</td>
<td style="padding-left:10px;vertical-align:middle;">
<strong style="font-size:15px;letter-spacing:1.2px;display:block;">PROLOGY IT</strong>
<span style="font-size:11px;color:#5f6978;font-weight:500;">Equipment Receiving &amp; Testing Report</span>
</td>
</tr>
</table>
</td>
<td align="right" style="vertical-align:middle;">
<span style="font-size:12px;font-weight:700;color:#3b82f6;font-family:'Courier New',monospace;display:block;">#${escapeHtml(reportId)}</span>
<span style="font-size:11px;color:#9ca3af;">${escapeHtml(reportDate)}</span>
</td>
</tr>
</table>
</td></tr>
</table>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:${verdictBg};border:1px solid ${verdictBd};border-radius:0 0 10px 10px;border-collapse:separate;">
<tr><td style="padding:9px 20px;color:${verdictTx};font-size:12px;font-weight:600;">
<svg viewBox="0 0 20 20" width="18" height="18" fill="none" style="vertical-align:middle;color:${verdictTx};"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.8"/>${verdictPathSvg}</svg>
<b style="letter-spacing:.8px;vertical-align:middle;margin-left:8px;">${verdictLabel}</b>
<span style="opacity:.7;font-weight:500;vertical-align:middle;margin-left:8px;">${escapeHtml(verdictMsg)}</span>
</td></tr>
</table>
</td></tr>
</table>
<!-- MAIN -->
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto;">
<!-- ZONE 1: AT-A-GLANCE Product Info + Tech Specs -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="50%" valign="top" style="padding-right:5px;">
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Product Info</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;width:68px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Name</td><td style="padding:3px 0;font-weight:500;vertical-align:top;"><strong>${productName}</strong></td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">P/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;">${productPN}</td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">S/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;">${productSN}</td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Type</td><td style="padding:3px 0;vertical-align:top;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Switch Layer 3</span></td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Cond.</td><td style="padding:3px 0;vertical-align:top;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">Refurb Grade A</span></td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Supplier</td><td style="padding:3px 0;vertical-align:top;font-weight:500;">TechData AU PO #TD-88432</td></tr>
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Warranty</td><td style="padding:3px 0;vertical-align:top;font-weight:500;">12 Months ( May 2027)</td></tr>
</table>
</td></tr>
</table>
</td>
<td width="50%" valign="top" style="padding-left:5px;">
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Technical Specs</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
<tr>
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Specification</th>
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Actual</th>
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Default</th>
</tr>
<tr>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">IOS-XE Version</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${iosVersion}</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">17.06.01</td>
</tr>
<tr>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">System RAM</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${memDisplay}</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">8 GB</td>
</tr>
<tr>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Flash Storage</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${flashDisplay}</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">16 GB</td>
</tr>
<tr>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Uplink Module</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">C9300-NM-4G</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-style:italic;color:#cbd5e1;font-family:Consolas,monospace;">N/A</td>
</tr>
<tr>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">PSU Model</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">715W AC</td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">715W AC</td>
</tr>
<tr>
<td style="padding:6px 0;font-weight:600;color:#5f6978;">PoE Budget</td>
<td style="padding:6px 0;font-weight:700;font-family:Consolas,monospace;">437 Watts</td>
<td style="padding:6px 0;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">437 Watts</td>
</tr>
</table>
</td></tr>
</table>
</td>
</tr>
</table>
</td></tr>
<!-- Issues Found -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Issues Found</div>
${aiIssueRowsHtml}
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:5px;border-collapse:separate;">
<tr>
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#fef3c7;color:#92400e;vertical-align:middle;">COSMETIC</span><span style="margin-left:8px;vertical-align:middle;">Minor scratch on top chassis (2cm) non-functional</span></td>
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">Accepted</span></td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:6px;border-collapse:separate;">
<tr>
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#ffedd5;color:#9a3412;vertical-align:middle;">MINOR</span><span style="margin-left:8px;vertical-align:middle;">Fan #2 at 48dB under stress (spec 45dB) within rack tolerance</span></td>
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Monitor</span></td>
</tr>
</table>
<div style="text-align:center;font-size:11px;color:#9ca3af;padding-top:6px;border-top:1px solid #f0f1f3;"><b>0</b> Critical · <b>0</b> Major · <b>1</b> Minor · <b>1</b> Cosmetic</div>
</td></tr>
</table>
</td></tr>
<!-- Receiving & Inspection Notes -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Receiving &amp; Inspection Notes</div>
<div style="padding:10px 14px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:12px;color:#92400e;margin-bottom:8px;">
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">&#9888; Warning from Warehouse</div>
<p style="margin:0;">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.</p>
</div>
<div style="padding:10px 14px;background:#f9fafb;border-left:3px solid #e5e7eb;border-radius:0 6px 6px 0;font-size:12px;color:#5f6978;">
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">Accessory Checklist</div>
<table cellpadding="0" cellspacing="0" border="0" style="margin-top:6px;">
<tr>
<td style="padding:0 4px 0 0;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Rackmount</span></td>
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>PSU (Internal)</span></td>
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Console Cable</span></td>
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Documents</span></td>
<td style="padding:0 0 0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Original Box</span></td>
</tr>
</table>
</div>
</td></tr>
</table>
</td></tr>
<!-- Inspection Log Workflow -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="33%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Received</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Trung Nguyen</div>
<div style="font-size:10px;color:#9ca3af;">06 May 10:30</div>
</td>
<td width="33%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Physical Check</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Khanh Le</div>
<div style="font-size:10px;color:#9ca3af;">06 May 11:15</div>
</td>
<td width="34%" align="center" style="padding:4px;">
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-bottom:8px;">&#10003;</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">Software Test</div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;">Duy Pham (remote)</div>
<div style="font-size:10px;color:#9ca3af;">06 May 14:00</div>
</td>
</tr>
</table>
</td></tr>
</table>
</td></tr>
<!-- Divider -->
<tr><td style="padding:6px 0;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;">&nbsp;</td>
<td width="60" align="center" style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#9ca3af;padding:0 12px;">Detail</td>
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;">&nbsp;</td>
</tr>
</table>
</td></tr>
<!-- Physical Check section -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
<tr>
<td style="padding:7px 12px;color:#166534;font-size:13px;font-weight:700;">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#166534;"><rect x="2" y="2" width="16" height="16" rx="3" stroke="currentColor" stroke-width="1.5"/><path d="M7 10h6M10 7v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
<span style="vertical-align:middle;margin-left:8px;">Physical Check</span>
</td>
<td align="right" style="padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65;">Khanh Le · 06 May 11:15</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="200" valign="top" style="padding-right:14px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="50%" style="padding:0 3px 6px 0;">${photoCellHtml('Front')}</td>
<td width="50%" style="padding:0 0 6px 3px;">${photoCellHtml('Rear')}</td>
</tr>
<tr>
<td width="50%" style="padding:0 3px 0 0;">${photoCellHtml('S/N Label')}</td>
<td width="50%" style="padding:0 0 0 3px;">${photoCellHtml('Package')}</td>
</tr>
</table>
</td>
<td valign="top">
${checklistRowsHtml}
</td>
</tr>
</table>
</td></tr>
</table>
</td></tr>
<!-- Software Check section -->
<tr><td style="padding-bottom:10px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
<tr><td style="padding:16px 20px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#eff6ff;border:1px solid #93c5fd;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
<tr>
<td style="padding:7px 12px;color:#1e40af;font-size:13px;font-weight:700;">
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#1e40af;"><rect x="2" y="3" width="16" height="11" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M7 17h6M10 14v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
<span style="vertical-align:middle;margin-left:8px;">Software Check</span>
</td>
<td align="right" style="padding:7px 12px;color:#1e40af;font-size:11px;font-weight:500;opacity:.65;">Duy Pham (remote) · 06 May 14:0017:45</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="33%" valign="top" style="padding-right:8px;">
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Hardware Inventory</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">${productPN}</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">${productSN}</td></tr>
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">PWR-C1-715WAC-P</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">LIT241525W1</td></tr>
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">C9300-NM-4G</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">FDO2420H0X1</td></tr>
<tr><td style="padding:2px 0;font-weight:600;color:#5f6978;">FAN-T2-GEN2 (x3)</td><td style="padding:2px 0;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">OK</td></tr>
</table>
</td>
<td width="33%" valign="top" style="padding:0 4px;">
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">System &amp; License</div>
${licenseBoxesHtml}
</td>
<td width="34%" valign="top" style="padding-left:8px;">
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Port Test Summary</div>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="50%" style="padding:0 3px 6px 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${poeColor};">${escapeHtml(poeText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">${hasPortData ? 'PoE UP' : 'GigE UP'}</div></td></tr></table></td>
<td width="50%" style="padding:0 0 6px 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${sfpColor};">${escapeHtml(sfpText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">SFP+ UP</div></td></tr></table></td>
</tr>
<tr>
<td width="50%" style="padding:0 3px 0 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:#10b981;">${missingSFP.length > 0 || missingPoE.length > 0 ? 'FAIL' : 'PASS'}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">PoE+ Test</div></td></tr></table></td>
<td width="50%" style="padding:0 0 0 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:#10b981;">${Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">Throughput</div></td></tr></table></td>
</tr>
</table>
${missingDetailsHtml}
</td>
</tr>
</table>
<!-- CONSOLE RAW OUTPUT -->
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top:16px;background:#1e293b;border-radius:6px;border:1px solid #334155;border-collapse:separate;">
<tr><td style="padding:6px 12px;background:#334155;color:#94a3b8;font-size:10px;font-weight:700;letter-spacing:.5px;border-radius:6px 6px 0 0;">CONSOLE RAW OUTPUT (Boot Log snippet)</td></tr>
<tr><pre style="overflow-y: auto; max-height: 300px; padding:12px;color:#cbd5e1;font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${snapshot?.outputTestLog || 'No test log available'}</pre></tr>
</table>
</td></tr>
</table>
</td></tr>
<!-- Footer -->
<tr><td align="center" style="padding-top:16px;font-size:10px;color:#9ca3af;">Prology IT Equipment QA System · Confidential Internal Use Only</td></tr>
</table>
</body>
</html>`
// 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)
}

View File

@ -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[]

View File

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