feat(ui): add copy report functionality to self-test

Add "Copy Report" button that generates a formatted diagnostic report:
- Device info (ID, modem version, timestamp)
- Test results summary with status icons
- Raw AT command responses for debugging
- Detailed execution logs
- Nicely formatted for sharing with manufacturer support

Enhanced logging with raw AT responses displayed in monospace format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-02-10 11:03:37 +01:00
parent 3a6b529cba
commit 3d61ce22d3

View File

@@ -424,17 +424,24 @@
<!-- Logs section --> <!-- Logs section -->
<div class="mt-3"> <div class="mt-3">
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#selftest_logs_collapse"> <button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#selftest_logs_collapse">
Show logs Show detailed logs
</button> </button>
<div class="collapse mt-2" id="selftest_logs_collapse"> <div class="collapse mt-2" id="selftest_logs_collapse">
<div class="card card-body bg-light" style="max-height: 200px; overflow-y: auto;"> <div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
<small><code id="selftest_logs"></code></small> <pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div id="selftest_summary" class="me-auto"></div> <div id="selftest_summary" class="me-auto"></div>
<button type="button" class="btn btn-outline-primary" id="selfTestCopyBtn" onclick="copySelfTestReport()" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard me-1" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>
Copy Report
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
</div> </div>
</div> </div>
@@ -1492,9 +1499,27 @@ function update_modem_configMode(param, checked){
// SELF TEST FUNCTIONS // SELF TEST FUNCTIONS
// ============================================ // ============================================
// Global object to store test results for report
let selfTestReport = {
timestamp: '',
deviceId: '',
modemVersion: '',
results: {},
rawResponses: {}
};
function runSelfTest() { function runSelfTest() {
console.log("Starting Self Test..."); console.log("Starting Self Test...");
// Reset report
selfTestReport = {
timestamp: new Date().toISOString(),
deviceId: document.querySelector('.sideBar_sensorName')?.textContent || 'Unknown',
modemVersion: document.getElementById('modem_version')?.textContent || 'Unknown',
results: {},
rawResponses: {}
};
// Reset UI // Reset UI
resetSelfTestUI(); resetSelfTestUI();
@@ -1502,9 +1527,10 @@ function runSelfTest() {
const modal = new bootstrap.Modal(document.getElementById('selfTestModal')); const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
modal.show(); modal.show();
// Disable close buttons during test // Disable buttons during test
document.getElementById('selfTestCloseBtn').disabled = true; document.getElementById('selfTestCloseBtn').disabled = true;
document.getElementById('selfTestDoneBtn').disabled = true; document.getElementById('selfTestDoneBtn').disabled = true;
document.getElementById('selfTestCopyBtn').disabled = true;
document.getElementById('btn_selfTest').disabled = true; document.getElementById('btn_selfTest').disabled = true;
// Start test sequence // Start test sequence
@@ -1547,10 +1573,17 @@ function resetSelfTestUI() {
document.getElementById('selftest_summary').innerHTML = ''; document.getElementById('selftest_summary').innerHTML = '';
} }
function addSelfTestLog(message) { function addSelfTestLog(message, isRaw = false) {
const logsEl = document.getElementById('selftest_logs'); const logsEl = document.getElementById('selftest_logs');
const timestamp = new Date().toLocaleTimeString(); const timestamp = new Date().toLocaleTimeString();
logsEl.innerHTML += `[${timestamp}] ${message}<br>`;
if (isRaw) {
// Raw AT response - format nicely
logsEl.textContent += `[${timestamp}] >>> RAW RESPONSE:\n${message}\n<<<\n`;
} else {
logsEl.textContent += `[${timestamp}] ${message}\n`;
}
// Auto-scroll to bottom // Auto-scroll to bottom
logsEl.parentElement.scrollTop = logsEl.parentElement.scrollHeight; logsEl.parentElement.scrollTop = logsEl.parentElement.scrollHeight;
} }
@@ -1559,6 +1592,12 @@ function updateTestStatus(testId, status, detail, badge) {
document.getElementById(`test_${testId}_status`).className = `badge ${badge}`; document.getElementById(`test_${testId}_status`).className = `badge ${badge}`;
document.getElementById(`test_${testId}_status`).textContent = status; document.getElementById(`test_${testId}_status`).textContent = status;
document.getElementById(`test_${testId}_detail`).textContent = detail; document.getElementById(`test_${testId}_detail`).textContent = detail;
// Store result in report
selfTestReport.results[testId] = {
status: status,
detail: detail
};
} }
function setConfigMode(enabled) { function setConfigMode(enabled) {
@@ -1591,18 +1630,24 @@ function setConfigMode(enabled) {
function sendATCommand(command, timeout) { function sendATCommand(command, timeout) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
addSelfTestLog(`Sending AT command: ${command}`); addSelfTestLog(`Sending AT command: ${command} (timeout: ${timeout}s)`);
$.ajax({ $.ajax({
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`, url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
dataType: 'text', dataType: 'text',
method: 'GET', method: 'GET',
success: function(response) { success: function(response) {
addSelfTestLog(`Response: ${response.replace(/\n/g, ' | ')}`); // Store raw response in report
selfTestReport.rawResponses[command] = response;
// Log raw response
addSelfTestLog(response.trim(), true);
resolve(response); resolve(response);
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
addSelfTestLog(`AT command error: ${error}`); addSelfTestLog(`AT command error: ${error}`);
selfTestReport.rawResponses[command] = `ERROR: ${error}`;
reject(new Error(error)); reject(new Error(error));
} }
}); });
@@ -1634,16 +1679,22 @@ async function selfTestSequence() {
dataType: 'json', dataType: 'json',
method: 'GET', method: 'GET',
success: function(data) { success: function(data) {
addSelfTestLog(`WiFi status: ${JSON.stringify(data)}`); addSelfTestLog(`WiFi status received`);
// Store raw response
selfTestReport.rawResponses['WiFi Status'] = JSON.stringify(data, null, 2);
resolve(data); resolve(data);
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
addSelfTestLog(`WiFi status error: ${error}`); addSelfTestLog(`WiFi status error: ${error}`);
selfTestReport.rawResponses['WiFi Status'] = `ERROR: ${error}`;
reject(new Error(error)); reject(new Error(error));
} }
}); });
}); });
// Log detailed WiFi info
addSelfTestLog(`Mode: ${wifiResponse.mode}, SSID: ${wifiResponse.ssid}, IP: ${wifiResponse.ip}, Hostname: ${wifiResponse.hostname}`);
if (wifiResponse.connected) { if (wifiResponse.connected) {
let modeIcon = ''; let modeIcon = '';
let modeLabel = ''; let modeLabel = '';
@@ -1907,15 +1958,128 @@ async function selfTestSequence() {
<span class="badge bg-success me-1">${testsPassed} passed</span> <span class="badge bg-success me-1">${testsPassed} passed</span>
<span class="badge bg-danger">${testsFailed} failed</span>`; <span class="badge bg-danger">${testsFailed} failed</span>`;
// Enable close buttons // Store summary in report
selfTestReport.summary = {
passed: testsPassed,
failed: testsFailed,
status: statusText
};
// Enable buttons
document.getElementById('selfTestCloseBtn').disabled = false; document.getElementById('selfTestCloseBtn').disabled = false;
document.getElementById('selfTestDoneBtn').disabled = false; document.getElementById('selfTestDoneBtn').disabled = false;
document.getElementById('selfTestCopyBtn').disabled = false;
document.getElementById('btn_selfTest').disabled = false; document.getElementById('btn_selfTest').disabled = false;
addSelfTestLog('Self test completed.'); addSelfTestLog('Self test completed.');
addSelfTestLog('Click "Copy Report" to share results with support.');
} }
} }
function copySelfTestReport() {
// Build formatted report
let report = `═══════════════════════════════════════════════════════════════
NEBULEAIR PRO 4G - SELF TEST REPORT
═══════════════════════════════════════════════════════════════
📅 Date: ${selfTestReport.timestamp}
🔧 Device ID: ${selfTestReport.deviceId}
📱 Modem Version: ${selfTestReport.modemVersion}
───────────────────────────────────────────────────────────────
TEST RESULTS
───────────────────────────────────────────────────────────────
`;
// Add test results
const testNames = {
wifi: '📡 WiFi/Network',
modem: '📟 Modem Connection',
sim: '💳 SIM Card',
signal: '📶 Signal Strength',
network: '🌐 Network Connection'
};
for (const [testId, name] of Object.entries(testNames)) {
if (selfTestReport.results[testId]) {
const result = selfTestReport.results[testId];
const statusIcon = result.status === 'Passed' ? '✅' :
result.status === 'Failed' ? '❌' :
result.status.includes('Hotspot') || result.status.includes('WiFi') || result.status.includes('Ethernet') ? '' : '⚠️';
report += `${name}
Status: ${statusIcon} ${result.status}
Detail: ${result.detail}
`;
}
}
// Add summary
if (selfTestReport.summary) {
report += `───────────────────────────────────────────────────────────────
SUMMARY
───────────────────────────────────────────────────────────────
✅ Passed: ${selfTestReport.summary.passed}
❌ Failed: ${selfTestReport.summary.failed}
📊 Status: ${selfTestReport.summary.status}
`;
}
// Add raw AT responses
report += `───────────────────────────────────────────────────────────────
RAW AT RESPONSES
───────────────────────────────────────────────────────────────
`;
for (const [command, response] of Object.entries(selfTestReport.rawResponses)) {
report += `Command: ${command}
Response:
${response}
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
`;
}
// Add full logs
report += `───────────────────────────────────────────────────────────────
DETAILED LOGS
───────────────────────────────────────────────────────────────
${document.getElementById('selftest_logs').textContent}
═══════════════════════════════════════════════════════════════
END OF REPORT - Generated by NebuleAir Pro 4G
═══════════════════════════════════════════════════════════════
`;
// Copy to clipboard
navigator.clipboard.writeText(report).then(function() {
// Show success feedback
const copyBtn = document.getElementById('selfTestCopyBtn');
const originalHtml = copyBtn.innerHTML;
copyBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check me-1" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
Copied!`;
copyBtn.classList.remove('btn-outline-primary');
copyBtn.classList.add('btn-success');
setTimeout(function() {
copyBtn.innerHTML = originalHtml;
copyBtn.classList.remove('btn-success');
copyBtn.classList.add('btn-outline-primary');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard. Please select and copy the logs manually.');
});
}
</script> </script>