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:
184
html/saraR4.html
184
html/saraR4.html
@@ -424,17 +424,24 @@
|
||||
<!-- Logs section -->
|
||||
<div class="mt-3">
|
||||
<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>
|
||||
<div class="collapse mt-2" id="selftest_logs_collapse">
|
||||
<div class="card card-body bg-light" style="max-height: 200px; overflow-y: auto;">
|
||||
<small><code id="selftest_logs"></code></small>
|
||||
<div class="card card-body bg-dark text-light" style="max-height: 250px; overflow-y: auto; font-family: monospace; font-size: 0.75rem;">
|
||||
<pre id="selftest_logs" style="margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1492,9 +1499,27 @@ function update_modem_configMode(param, checked){
|
||||
// SELF TEST FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
// Global object to store test results for report
|
||||
let selfTestReport = {
|
||||
timestamp: '',
|
||||
deviceId: '',
|
||||
modemVersion: '',
|
||||
results: {},
|
||||
rawResponses: {}
|
||||
};
|
||||
|
||||
function runSelfTest() {
|
||||
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
|
||||
resetSelfTestUI();
|
||||
|
||||
@@ -1502,9 +1527,10 @@ function runSelfTest() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
|
||||
modal.show();
|
||||
|
||||
// Disable close buttons during test
|
||||
// Disable buttons during test
|
||||
document.getElementById('selfTestCloseBtn').disabled = true;
|
||||
document.getElementById('selfTestDoneBtn').disabled = true;
|
||||
document.getElementById('selfTestCopyBtn').disabled = true;
|
||||
document.getElementById('btn_selfTest').disabled = true;
|
||||
|
||||
// Start test sequence
|
||||
@@ -1547,10 +1573,17 @@ function resetSelfTestUI() {
|
||||
document.getElementById('selftest_summary').innerHTML = '';
|
||||
}
|
||||
|
||||
function addSelfTestLog(message) {
|
||||
function addSelfTestLog(message, isRaw = false) {
|
||||
const logsEl = document.getElementById('selftest_logs');
|
||||
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
|
||||
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`).textContent = status;
|
||||
document.getElementById(`test_${testId}_detail`).textContent = detail;
|
||||
|
||||
// Store result in report
|
||||
selfTestReport.results[testId] = {
|
||||
status: status,
|
||||
detail: detail
|
||||
};
|
||||
}
|
||||
|
||||
function setConfigMode(enabled) {
|
||||
@@ -1591,18 +1630,24 @@ function setConfigMode(enabled) {
|
||||
|
||||
function sendATCommand(command, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
addSelfTestLog(`Sending AT command: ${command}`);
|
||||
addSelfTestLog(`Sending AT command: ${command} (timeout: ${timeout}s)`);
|
||||
|
||||
$.ajax({
|
||||
url: `launcher.php?type=sara&port=ttyAMA2&command=${encodeURIComponent(command)}&timeout=${timeout}`,
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
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);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
addSelfTestLog(`AT command error: ${error}`);
|
||||
selfTestReport.rawResponses[command] = `ERROR: ${error}`;
|
||||
reject(new Error(error));
|
||||
}
|
||||
});
|
||||
@@ -1634,16 +1679,22 @@ async function selfTestSequence() {
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
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);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
addSelfTestLog(`WiFi status error: ${error}`);
|
||||
selfTestReport.rawResponses['WiFi Status'] = `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) {
|
||||
let modeIcon = '';
|
||||
let modeLabel = '';
|
||||
@@ -1907,15 +1958,128 @@ async function selfTestSequence() {
|
||||
<span class="badge bg-success me-1">${testsPassed} passed</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('selfTestDoneBtn').disabled = false;
|
||||
document.getElementById('selfTestCopyBtn').disabled = false;
|
||||
document.getElementById('btn_selfTest').disabled = false;
|
||||
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user