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 -->
|
<!-- 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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user