v1.4.4 — Self-test partagé sur Accueil/Capteurs/Admin + test RTC DS3231
Extraction du code self-test dans des fichiers partagés (selftest.js + selftest-modal.html) pour éviter la duplication. Ajout du bouton Run Self Test sur les pages index, sensors et admin. Nouveau test RTC qui vérifie la connexion du module DS3231 et la synchronisation horloge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.4.4",
|
||||
"date": "2026-03-16",
|
||||
"changes": {
|
||||
"features": [
|
||||
"Bouton Self Test disponible sur les pages Accueil, Capteurs et Admin (en plus de Modem 4G)",
|
||||
"Test du module RTC DS3231 integre dans le self-test (connexion + synchronisation horloge)"
|
||||
],
|
||||
"improvements": [
|
||||
"Refactoring self-test : code JS et HTML des modals extraits dans des fichiers partages (selftest.js, selftest-modal.html)",
|
||||
"Le modal self-test est charge dynamiquement via fetch, plus besoin de dupliquer le HTML"
|
||||
],
|
||||
"fixes": [],
|
||||
"compatibility": []
|
||||
},
|
||||
"notes": "Le self-test est maintenant accessible depuis toutes les pages principales. Le test RTC verifie la connexion du module et l'ecart avec l'heure systeme."
|
||||
},
|
||||
{
|
||||
"version": "1.4.3",
|
||||
"date": "2026-03-16",
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||
<h1 class="mt-4">Admin</h1>
|
||||
|
||||
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||
</svg>
|
||||
Run Self Test
|
||||
</button>
|
||||
|
||||
<div class="row mb-3">
|
||||
|
||||
@@ -401,6 +408,7 @@
|
||||
<!-- i18n translation system -->
|
||||
<script src="assets/js/i18n.js"></script>
|
||||
<script src="assets/js/topbar-logo.js"></script>
|
||||
<script src="assets/js/selftest.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
943
html/assets/js/selftest.js
Normal file
943
html/assets/js/selftest.js
Normal file
@@ -0,0 +1,943 @@
|
||||
// ============================================
|
||||
// SELF TEST FUNCTIONS (shared across pages)
|
||||
// ============================================
|
||||
|
||||
// Cache for operators data
|
||||
let operatorsDataSelfTest = null;
|
||||
|
||||
function loadOperatorsDataSelfTest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (operatorsDataSelfTest) {
|
||||
resolve(operatorsDataSelfTest);
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
url: 'assets/data/operators.json',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
operatorsDataSelfTest = data;
|
||||
resolve(data);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Failed to load operators data:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('selfTestModal'));
|
||||
modal.show();
|
||||
|
||||
// Disable buttons during test
|
||||
document.getElementById('selfTestCloseBtn').disabled = true;
|
||||
document.getElementById('selfTestDoneBtn').disabled = true;
|
||||
document.getElementById('selfTestCopyBtn').disabled = true;
|
||||
document.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = true);
|
||||
|
||||
// Start test sequence
|
||||
selfTestSequence();
|
||||
}
|
||||
|
||||
function resetSelfTestUI() {
|
||||
// Reset status
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Preparing test...</span>
|
||||
</div>`;
|
||||
|
||||
// Reset test items
|
||||
document.getElementById('test_wifi_status').className = 'badge bg-secondary';
|
||||
document.getElementById('test_wifi_status').textContent = 'Pending';
|
||||
document.getElementById('test_wifi_detail').textContent = 'Waiting...';
|
||||
|
||||
document.getElementById('test_modem_status').className = 'badge bg-secondary';
|
||||
document.getElementById('test_modem_status').textContent = 'Pending';
|
||||
document.getElementById('test_modem_detail').textContent = 'Waiting...';
|
||||
|
||||
document.getElementById('test_sim_status').className = 'badge bg-secondary';
|
||||
document.getElementById('test_sim_status').textContent = 'Pending';
|
||||
document.getElementById('test_sim_detail').textContent = 'Waiting...';
|
||||
|
||||
document.getElementById('test_signal_status').className = 'badge bg-secondary';
|
||||
document.getElementById('test_signal_status').textContent = 'Pending';
|
||||
document.getElementById('test_signal_detail').textContent = 'Waiting...';
|
||||
|
||||
document.getElementById('test_network_status').className = 'badge bg-secondary';
|
||||
document.getElementById('test_network_status').textContent = 'Pending';
|
||||
document.getElementById('test_network_detail').textContent = 'Waiting...';
|
||||
|
||||
// Reset sensor tests
|
||||
document.getElementById('sensor_tests_container').innerHTML = '';
|
||||
document.getElementById('comm_tests_separator').style.display = 'none';
|
||||
|
||||
// Reset logs
|
||||
document.getElementById('selftest_logs').innerHTML = '';
|
||||
|
||||
// Reset summary
|
||||
document.getElementById('selftest_summary').innerHTML = '';
|
||||
}
|
||||
|
||||
function addSelfTestLog(message, isRaw = false) {
|
||||
const logsEl = document.getElementById('selftest_logs');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
addSelfTestLog(`Setting modem_config_mode to ${enabled}...`);
|
||||
|
||||
$.ajax({
|
||||
url: `launcher.php?type=update_config_sqlite¶m=modem_config_mode&value=${enabled}`,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
addSelfTestLog(`modem_config_mode set to ${enabled}`);
|
||||
// Update checkbox state if it exists on the page
|
||||
const checkbox = document.getElementById('check_modem_configMode');
|
||||
if (checkbox) checkbox.checked = enabled;
|
||||
resolve(true);
|
||||
} else {
|
||||
addSelfTestLog(`Failed to set modem_config_mode: ${response.error || 'Unknown error'}`);
|
||||
reject(new Error(response.error || 'Failed to set config mode'));
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
addSelfTestLog(`AJAX error setting config mode: ${error}`);
|
||||
reject(new Error(error));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendATCommand(command, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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) {
|
||||
// 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delaySelfTest(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function selfTestSequence() {
|
||||
let testsPassed = 0;
|
||||
let testsFailed = 0;
|
||||
|
||||
try {
|
||||
// Collect system info at the start
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Collecting system information...</span>
|
||||
</div>`;
|
||||
|
||||
// Get system info from config
|
||||
try {
|
||||
const configResponse = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=get_config_sqlite',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error)); }
|
||||
});
|
||||
});
|
||||
|
||||
// Store in report
|
||||
selfTestReport.deviceId = configResponse.deviceID || 'Unknown';
|
||||
selfTestReport.deviceName = configResponse.deviceName || 'Unknown';
|
||||
selfTestReport.modemVersion = configResponse.modem_version || 'Unknown';
|
||||
selfTestReport.latitude = configResponse.latitude_raw || 'N/A';
|
||||
selfTestReport.longitude = configResponse.longitude_raw || 'N/A';
|
||||
selfTestReport.config = configResponse;
|
||||
|
||||
// Get RTC time
|
||||
try {
|
||||
const rtcTime = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=RTC_time',
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
success: function(data) { resolve(data.trim()); },
|
||||
error: function(xhr, status, error) { resolve('N/A'); }
|
||||
});
|
||||
});
|
||||
selfTestReport.systemTime = rtcTime;
|
||||
} catch (e) {
|
||||
selfTestReport.systemTime = 'N/A';
|
||||
}
|
||||
|
||||
// Log system info
|
||||
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||
addSelfTestLog(' NEBULEAIR PRO 4G - SELF TEST');
|
||||
addSelfTestLog('════════════════════════════════════════════════════════');
|
||||
addSelfTestLog(`Device ID: ${selfTestReport.deviceId}`);
|
||||
addSelfTestLog(`Device Name: ${selfTestReport.deviceName}`);
|
||||
addSelfTestLog(`Modem Version: ${selfTestReport.modemVersion}`);
|
||||
addSelfTestLog(`System Time (RTC): ${selfTestReport.systemTime}`);
|
||||
addSelfTestLog(`Browser Time: ${new Date().toLocaleString()}`);
|
||||
addSelfTestLog(`GPS: ${selfTestReport.latitude}, ${selfTestReport.longitude}`);
|
||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||
addSelfTestLog('');
|
||||
|
||||
} catch (error) {
|
||||
addSelfTestLog(`Warning: Could not get system config: ${error.message}`);
|
||||
}
|
||||
|
||||
await delaySelfTest(300);
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// SENSOR TESTS - Test enabled sensors based on config
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const config = selfTestReport.config || {};
|
||||
const sensorTests = [];
|
||||
|
||||
// NPM is always present
|
||||
sensorTests.push({ id: 'npm', name: 'NextPM (Particles)', type: 'npm', port: 'ttyAMA5' });
|
||||
|
||||
// BME280 if enabled
|
||||
if (config.BME280) {
|
||||
sensorTests.push({ id: 'bme280', name: 'BME280 (Temp/Hum)', type: 'BME280' });
|
||||
}
|
||||
|
||||
// Noise if enabled
|
||||
if (config.NOISE) {
|
||||
sensorTests.push({ id: 'noise', name: 'Noise Sensor', type: 'noise' });
|
||||
}
|
||||
|
||||
// Envea if enabled
|
||||
if (config.envea) {
|
||||
sensorTests.push({ id: 'envea', name: 'Envea (Gas Sensors)', type: 'envea' });
|
||||
}
|
||||
|
||||
// RTC module is always present (DS3231)
|
||||
sensorTests.push({ id: 'rtc', name: 'RTC Module (DS3231)', type: 'rtc' });
|
||||
|
||||
// Create sensor test UI entries dynamically
|
||||
const sensorContainer = document.getElementById('sensor_tests_container');
|
||||
sensorContainer.innerHTML = '';
|
||||
|
||||
sensorTests.forEach(sensor => {
|
||||
sensorContainer.innerHTML += `
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_${sensor.id}">
|
||||
<div>
|
||||
<strong>${sensor.name}</strong>
|
||||
<div class="small text-muted" id="test_${sensor.id}_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_${sensor.id}_status" class="badge bg-secondary">Pending</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
addSelfTestLog('');
|
||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||
addSelfTestLog('SENSOR TESTS');
|
||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||
|
||||
// Run each sensor test
|
||||
for (const sensor of sensorTests) {
|
||||
await delaySelfTest(500);
|
||||
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Testing ${sensor.name}...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus(sensor.id, 'Testing...', 'Reading sensor data...', 'bg-info');
|
||||
addSelfTestLog(`Testing ${sensor.name}...`);
|
||||
|
||||
try {
|
||||
if (sensor.type === 'npm') {
|
||||
// NPM sensor test
|
||||
const npmResult = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=npm&port=' + sensor.port,
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||
});
|
||||
});
|
||||
|
||||
selfTestReport.rawResponses['NPM Sensor'] = JSON.stringify(npmResult, null, 2);
|
||||
addSelfTestLog(`NPM response: PM1=${npmResult.PM1}, PM2.5=${npmResult.PM25}, PM10=${npmResult.PM10}`);
|
||||
|
||||
// Check for errors
|
||||
const npmErrors = ['notReady', 'fanError', 'laserError', 'heatError', 't_rhError', 'memoryError', 'degradedState'];
|
||||
const activeErrors = npmErrors.filter(e => npmResult[e] === 1);
|
||||
|
||||
if (activeErrors.length > 0) {
|
||||
updateTestStatus(sensor.id, 'Warning', `Errors: ${activeErrors.join(', ')}`, 'bg-warning');
|
||||
testsFailed++;
|
||||
} else if (npmResult.PM1 !== undefined && npmResult.PM25 !== undefined && npmResult.PM10 !== undefined) {
|
||||
updateTestStatus(sensor.id, 'Passed', `PM1: ${npmResult.PM1} | PM2.5: ${npmResult.PM25} | PM10: ${npmResult.PM10} ug/m3`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
} else if (sensor.type === 'BME280') {
|
||||
// BME280 sensor test
|
||||
const bme280Result = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=BME280',
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||
});
|
||||
});
|
||||
|
||||
const bmeData = JSON.parse(bme280Result);
|
||||
selfTestReport.rawResponses['BME280 Sensor'] = JSON.stringify(bmeData, null, 2);
|
||||
addSelfTestLog(`BME280 response: temp=${bmeData.temp}, hum=${bmeData.hum}, press=${bmeData.press}`);
|
||||
|
||||
if (bmeData.temp !== undefined && bmeData.hum !== undefined && bmeData.press !== undefined) {
|
||||
updateTestStatus(sensor.id, 'Passed', `${bmeData.temp}°C | ${bmeData.hum}% | ${bmeData.press} hPa`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Warning', 'Incomplete data received', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
} else if (sensor.type === 'noise') {
|
||||
// NSRT MK4 noise sensor test (returns JSON)
|
||||
const noiseResult = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=noise',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||
});
|
||||
});
|
||||
|
||||
selfTestReport.rawResponses['Noise Sensor'] = JSON.stringify(noiseResult);
|
||||
addSelfTestLog(`Noise response: ${JSON.stringify(noiseResult)}`);
|
||||
|
||||
if (noiseResult.error) {
|
||||
updateTestStatus(sensor.id, 'Failed', noiseResult.error, 'bg-danger');
|
||||
testsFailed++;
|
||||
} else if (noiseResult.LEQ > 0 && noiseResult.dBA > 0) {
|
||||
updateTestStatus(sensor.id, 'Passed', `LEQ: ${noiseResult.LEQ} dB | dB(A): ${noiseResult.dBA}`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Warning', `Unexpected values: LEQ=${noiseResult.LEQ}, dBA=${noiseResult.dBA}`, 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
} else if (sensor.type === 'envea') {
|
||||
// Envea sensor test - use the debug endpoint for all sensors
|
||||
const enveaResult = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=envea_debug',
|
||||
dataType: 'text',
|
||||
method: 'GET',
|
||||
timeout: 30000,
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||
});
|
||||
});
|
||||
|
||||
selfTestReport.rawResponses['Envea Sensors'] = enveaResult;
|
||||
addSelfTestLog(`Envea response: ${enveaResult.trim().substring(0, 200)}`);
|
||||
|
||||
if (enveaResult.trim() !== '' && !enveaResult.toLowerCase().includes('error')) {
|
||||
updateTestStatus(sensor.id, 'Passed', 'Sensors responding', 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (enveaResult.toLowerCase().includes('error')) {
|
||||
updateTestStatus(sensor.id, 'Failed', 'Sensor error detected', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Failed', 'No data received', 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
} else if (sensor.type === 'rtc') {
|
||||
// RTC DS3231 module test
|
||||
const rtcResult = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=sys_RTC_module_time',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
success: function(data) { resolve(data); },
|
||||
error: function(xhr, status, error) { reject(new Error(error || status)); }
|
||||
});
|
||||
});
|
||||
|
||||
selfTestReport.rawResponses['RTC Module'] = JSON.stringify(rtcResult, null, 2);
|
||||
addSelfTestLog(`RTC response: ${JSON.stringify(rtcResult)}`);
|
||||
|
||||
if (rtcResult.rtc_module_time === 'not connected') {
|
||||
updateTestStatus(sensor.id, 'Failed', 'RTC module not connected', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else if (rtcResult.rtc_module_time) {
|
||||
const timeDiff = rtcResult.time_difference_seconds;
|
||||
if (typeof timeDiff === 'number' && Math.abs(timeDiff) <= 60) {
|
||||
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time} (sync OK, diff: ${timeDiff}s)`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (typeof timeDiff === 'number') {
|
||||
updateTestStatus(sensor.id, 'Warning', `${rtcResult.rtc_module_time} (out of sync: ${timeDiff}s)`, 'bg-warning');
|
||||
testsFailed++;
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Passed', `${rtcResult.rtc_module_time}`, 'bg-success');
|
||||
testsPassed++;
|
||||
}
|
||||
} else {
|
||||
updateTestStatus(sensor.id, 'Warning', 'Unexpected response', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addSelfTestLog(`${sensor.name} test error: ${error.message}`);
|
||||
updateTestStatus(sensor.id, 'Failed', error.message, 'bg-danger');
|
||||
selfTestReport.rawResponses[`${sensor.name}`] = `ERROR: ${error.message}`;
|
||||
testsFailed++;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// COMMUNICATION TESTS - WiFi, Modem, SIM, Signal, Network
|
||||
// ═══════════════════════════════════════════════════════
|
||||
addSelfTestLog('');
|
||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||
addSelfTestLog('COMMUNICATION TESTS');
|
||||
addSelfTestLog('────────────────────────────────────────────────────────');
|
||||
|
||||
document.getElementById('comm_tests_separator').style.display = '';
|
||||
|
||||
// Check WiFi / Network status (informational, no pass/fail)
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Checking network status...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus('wifi', 'Checking...', 'Getting network info...', 'bg-info');
|
||||
|
||||
try {
|
||||
const wifiResponse = await new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: 'launcher.php?type=wifi_status',
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
success: function(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 modeLabel = '';
|
||||
let badgeClass = 'bg-info';
|
||||
|
||||
if (wifiResponse.mode === 'hotspot') {
|
||||
modeLabel = 'Hotspot';
|
||||
badgeClass = 'bg-warning text-dark';
|
||||
} else if (wifiResponse.mode === 'wifi') {
|
||||
modeLabel = 'WiFi';
|
||||
badgeClass = 'bg-info';
|
||||
} else if (wifiResponse.mode === 'ethernet') {
|
||||
modeLabel = 'Ethernet';
|
||||
badgeClass = 'bg-info';
|
||||
}
|
||||
|
||||
const detailText = `${wifiResponse.ssid} | ${wifiResponse.ip} | ${wifiResponse.hostname}.local`;
|
||||
updateTestStatus('wifi', modeLabel, detailText, badgeClass);
|
||||
} else {
|
||||
updateTestStatus('wifi', 'Disconnected', 'No network connection', 'bg-secondary');
|
||||
}
|
||||
} catch (error) {
|
||||
updateTestStatus('wifi', 'Error', error.message, 'bg-secondary');
|
||||
}
|
||||
|
||||
await delaySelfTest(500);
|
||||
|
||||
// Enable config mode
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Enabling configuration mode...</span>
|
||||
</div>`;
|
||||
|
||||
await setConfigMode(true);
|
||||
|
||||
// Wait for SARA script to release the port (2 seconds should be enough)
|
||||
addSelfTestLog('Waiting for modem port to be available...');
|
||||
await delaySelfTest(2000);
|
||||
|
||||
// Test Modem Connection (ATI)
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Testing modem connection...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus('modem', 'Testing...', 'Sending ATI command...', 'bg-info');
|
||||
|
||||
try {
|
||||
const modemResponse = await sendATCommand('ATI', 5);
|
||||
|
||||
if (modemResponse.includes('OK') && (modemResponse.toUpperCase().includes('SARA-R5') || modemResponse.toUpperCase().includes('SARA-R4'))) {
|
||||
// Extract model
|
||||
const modelMatch = modemResponse.match(/SARA-R[45]\d*[A-Z]*-\d+[A-Z]*-\d+/i);
|
||||
const model = modelMatch ? modelMatch[0] : 'SARA module';
|
||||
updateTestStatus('modem', 'Passed', `Model: ${model}`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (modemResponse.includes('OK')) {
|
||||
updateTestStatus('modem', 'Passed', 'Modem responding', 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus('modem', 'Failed', 'No valid response', 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
} catch (error) {
|
||||
updateTestStatus('modem', 'Failed', error.message, 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
// Delay between AT commands
|
||||
await delaySelfTest(1000);
|
||||
|
||||
// Test SIM Card (AT+CCID?)
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Testing SIM card...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus('sim', 'Testing...', 'Sending AT+CCID? command...', 'bg-info');
|
||||
|
||||
try {
|
||||
const simResponse = await sendATCommand('AT+CCID?', 5);
|
||||
|
||||
const ccidMatch = simResponse.match(/\+CCID:\s*(\d{18,22})/);
|
||||
if (simResponse.includes('OK') && ccidMatch) {
|
||||
const iccid = ccidMatch[1];
|
||||
// Show last 4 digits only for privacy
|
||||
const maskedIccid = '****' + iccid.slice(-4);
|
||||
updateTestStatus('sim', 'Passed', `ICCID: ...${maskedIccid}`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (simResponse.includes('ERROR')) {
|
||||
updateTestStatus('sim', 'Failed', 'SIM card not detected', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else {
|
||||
updateTestStatus('sim', 'Warning', 'Unable to read ICCID', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
} catch (error) {
|
||||
updateTestStatus('sim', 'Failed', error.message, 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
// Delay between AT commands
|
||||
await delaySelfTest(1000);
|
||||
|
||||
// Test Signal Strength (AT+CSQ)
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Testing signal strength...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus('signal', 'Testing...', 'Sending AT+CSQ command...', 'bg-info');
|
||||
|
||||
try {
|
||||
const signalResponse = await sendATCommand('AT+CSQ', 5);
|
||||
|
||||
const csqMatch = signalResponse.match(/\+CSQ:\s*(\d+),(\d+)/);
|
||||
if (signalResponse.includes('OK') && csqMatch) {
|
||||
const signalPower = parseInt(csqMatch[1]);
|
||||
|
||||
if (signalPower === 99) {
|
||||
updateTestStatus('signal', 'Failed', 'No signal detected', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else if (signalPower === 0) {
|
||||
updateTestStatus('signal', 'Warning', 'Very poor signal (0/31)', 'bg-warning');
|
||||
testsFailed++;
|
||||
} else if (signalPower <= 24) {
|
||||
updateTestStatus('signal', 'Passed', `Poor signal (${signalPower}/31)`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (signalPower <= 26) {
|
||||
updateTestStatus('signal', 'Passed', `Good signal (${signalPower}/31)`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else if (signalPower <= 28) {
|
||||
updateTestStatus('signal', 'Passed', `Very good signal (${signalPower}/31)`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus('signal', 'Passed', `Excellent signal (${signalPower}/31)`, 'bg-success');
|
||||
testsPassed++;
|
||||
}
|
||||
} else if (signalResponse.includes('ERROR')) {
|
||||
updateTestStatus('signal', 'Failed', 'Unable to read signal', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else {
|
||||
updateTestStatus('signal', 'Warning', 'Unexpected response', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
} catch (error) {
|
||||
updateTestStatus('signal', 'Failed', error.message, 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
// Delay between AT commands
|
||||
await delaySelfTest(1000);
|
||||
|
||||
// Test Network Connection (AT+COPS?)
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Testing network connection...</span>
|
||||
</div>`;
|
||||
|
||||
updateTestStatus('network', 'Testing...', 'Sending AT+COPS? command...', 'bg-info');
|
||||
|
||||
try {
|
||||
// Load operators data for network name lookup
|
||||
let opData = null;
|
||||
try {
|
||||
opData = await loadOperatorsDataSelfTest();
|
||||
} catch (e) {
|
||||
addSelfTestLog('Warning: Could not load operators data');
|
||||
}
|
||||
|
||||
const networkResponse = await sendATCommand('AT+COPS?', 5);
|
||||
|
||||
const copsMatch = networkResponse.match(/\+COPS:\s*(\d+)(?:,(\d+),"?([^",]+)"?,(\d+))?/);
|
||||
if (networkResponse.includes('OK') && copsMatch) {
|
||||
const mode = copsMatch[1];
|
||||
const oper = copsMatch[3];
|
||||
const act = copsMatch[4];
|
||||
|
||||
if (oper) {
|
||||
// Get operator name from lookup table
|
||||
let operatorName = oper;
|
||||
if (opData && opData.operators && opData.operators[oper]) {
|
||||
operatorName = opData.operators[oper].name;
|
||||
}
|
||||
|
||||
// Get access technology
|
||||
let actDesc = 'Unknown';
|
||||
if (opData && opData.accessTechnology && opData.accessTechnology[act]) {
|
||||
actDesc = opData.accessTechnology[act];
|
||||
}
|
||||
|
||||
updateTestStatus('network', 'Passed', `${operatorName} (${actDesc})`, 'bg-success');
|
||||
testsPassed++;
|
||||
} else {
|
||||
updateTestStatus('network', 'Warning', 'Not registered to network', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
} else if (networkResponse.includes('ERROR')) {
|
||||
updateTestStatus('network', 'Failed', 'Unable to get network info', 'bg-danger');
|
||||
testsFailed++;
|
||||
} else {
|
||||
updateTestStatus('network', 'Warning', 'Unexpected response', 'bg-warning');
|
||||
testsFailed++;
|
||||
}
|
||||
} catch (error) {
|
||||
updateTestStatus('network', 'Failed', error.message, 'bg-danger');
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addSelfTestLog(`Test sequence error: ${error.message}`);
|
||||
} finally {
|
||||
// Always disable config mode at the end
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center text-primary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Disabling configuration mode...</span>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
await delaySelfTest(500);
|
||||
await setConfigMode(false);
|
||||
} catch (error) {
|
||||
addSelfTestLog(`Warning: Failed to disable config mode: ${error.message}`);
|
||||
}
|
||||
|
||||
// Show final status
|
||||
const totalTests = testsPassed + testsFailed;
|
||||
let statusClass, statusIcon, statusText;
|
||||
|
||||
if (testsFailed === 0) {
|
||||
statusClass = 'text-success';
|
||||
statusIcon = '✓';
|
||||
statusText = 'All tests passed';
|
||||
} else if (testsPassed === 0) {
|
||||
statusClass = 'text-danger';
|
||||
statusIcon = '✗';
|
||||
statusText = 'All tests failed';
|
||||
} else {
|
||||
statusClass = 'text-warning';
|
||||
statusIcon = '!';
|
||||
statusText = 'Some tests failed';
|
||||
}
|
||||
|
||||
document.getElementById('selftest_status').innerHTML = `
|
||||
<div class="d-flex align-items-center ${statusClass}">
|
||||
<span class="fs-4 me-2">${statusIcon}</span>
|
||||
<span><strong>${statusText}</strong></span>
|
||||
</div>`;
|
||||
|
||||
document.getElementById('selftest_summary').innerHTML = `
|
||||
<span class="badge bg-success me-1">${testsPassed} passed</span>
|
||||
<span class="badge bg-danger">${testsFailed} failed</span>`;
|
||||
|
||||
// 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.querySelectorAll('.btn_selfTest').forEach(btn => btn.disabled = false);
|
||||
|
||||
addSelfTestLog('Self test completed.');
|
||||
addSelfTestLog('Click "Copy Report" to share results with support.');
|
||||
}
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
// Build formatted report
|
||||
let report = `===============================================================
|
||||
NEBULEAIR PRO 4G - SELF TEST REPORT
|
||||
===============================================================
|
||||
|
||||
DEVICE INFORMATION
|
||||
------------------
|
||||
Device ID: ${selfTestReport.deviceId || 'Unknown'}
|
||||
Device Name: ${selfTestReport.deviceName || 'Unknown'}
|
||||
Modem Version: ${selfTestReport.modemVersion || 'Unknown'}
|
||||
System Time: ${selfTestReport.systemTime || 'Unknown'}
|
||||
Report Date: ${selfTestReport.timestamp}
|
||||
GPS Location: ${selfTestReport.latitude || 'N/A'}, ${selfTestReport.longitude || 'N/A'}
|
||||
|
||||
===============================================================
|
||||
TEST RESULTS
|
||||
===============================================================
|
||||
|
||||
`;
|
||||
|
||||
// Add test results (sensors first, then communication)
|
||||
const testNames = {
|
||||
npm: 'NextPM (Particles)',
|
||||
bme280: 'BME280 (Temp/Hum)',
|
||||
noise: 'Noise Sensor',
|
||||
envea: 'Envea (Gas Sensors)',
|
||||
rtc: 'RTC Module (DS3231)',
|
||||
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' ? '[OK]' :
|
||||
result.status === 'Failed' ? '[FAIL]' :
|
||||
result.status.includes('Hotspot') || result.status.includes('WiFi') || result.status.includes('Ethernet') ? '[INFO]' : '[WARN]';
|
||||
report += `${statusIcon} ${name}
|
||||
Status: ${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} ---
|
||||
${response}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
// Add full logs
|
||||
report += `===============================================================
|
||||
DETAILED LOGS
|
||||
===============================================================
|
||||
|
||||
${document.getElementById('selftest_logs').textContent}
|
||||
|
||||
===============================================================
|
||||
END OF REPORT - Generated by NebuleAir Pro 4G
|
||||
===============================================================
|
||||
`;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
function openShareReportModal() {
|
||||
// Generate the report
|
||||
const report = generateReport();
|
||||
|
||||
// Put report in textarea
|
||||
document.getElementById('shareReportText').value = report;
|
||||
|
||||
// Open the share modal
|
||||
const shareModal = new bootstrap.Modal(document.getElementById('shareReportModal'));
|
||||
shareModal.show();
|
||||
}
|
||||
|
||||
function selectAllReportText() {
|
||||
const textarea = document.getElementById('shareReportText');
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, textarea.value.length); // For mobile devices
|
||||
}
|
||||
|
||||
function downloadReport() {
|
||||
const report = generateReport();
|
||||
|
||||
// Create filename with device ID
|
||||
const deviceId = selfTestReport.deviceId || 'unknown';
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const filename = `logs_nebuleair_${deviceId}_${date}.txt`;
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([report], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Cleanup
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Load the self-test modal HTML into the page
|
||||
function initSelfTestModal() {
|
||||
fetch('selftest-modal.html')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// Insert modal HTML before </body>
|
||||
const container = document.createElement('div');
|
||||
container.id = 'selftest-modal-container';
|
||||
container.innerHTML = html;
|
||||
document.body.appendChild(container);
|
||||
})
|
||||
.catch(error => console.error('Error loading selftest modal:', error));
|
||||
}
|
||||
|
||||
// Auto-init when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initSelfTestModal);
|
||||
@@ -63,6 +63,14 @@
|
||||
<h1 class="mt-4" data-i18n="home.title">Votre capteur</h1>
|
||||
<p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p>
|
||||
|
||||
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||
</svg>
|
||||
Run Self Test
|
||||
</button>
|
||||
|
||||
<div class="row mb-3">
|
||||
|
||||
<!-- Card NPM values -->
|
||||
@@ -124,6 +132,7 @@
|
||||
<!-- i18n translation system -->
|
||||
<script src="assets/js/i18n.js"></script>
|
||||
<script src="assets/js/topbar-logo.js"></script>
|
||||
<script src="assets/js/selftest.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
1002
html/saraR4.html
1002
html/saraR4.html
File diff suppressed because it is too large
Load Diff
134
html/selftest-modal.html
Normal file
134
html/selftest-modal.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!-- Self Test Modal -->
|
||||
<div class="modal fade" id="selfTestModal" tabindex="-1" aria-labelledby="selfTestModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="selfTestModalLabel">Modem Self Test</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="selfTestCloseBtn" disabled></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="selftest_status" class="mb-3">
|
||||
<div class="d-flex align-items-center text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<span>Preparing test...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-group" id="selftest_results">
|
||||
<!-- Dynamic sensor test entries will be added here -->
|
||||
<div id="sensor_tests_container"></div>
|
||||
|
||||
<!-- Separator for communication tests -->
|
||||
<div id="comm_tests_separator" class="list-group-item bg-light text-center py-1" style="display:none;">
|
||||
<small class="text-muted fw-bold">COMMUNICATION</small>
|
||||
</div>
|
||||
|
||||
<!-- Info: WiFi Status -->
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_wifi">
|
||||
<div>
|
||||
<strong>WiFi / Network</strong>
|
||||
<div class="small text-muted" id="test_wifi_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_wifi_status" class="badge bg-secondary">Pending</span>
|
||||
</div>
|
||||
|
||||
<!-- Test: Modem Connection -->
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_modem">
|
||||
<div>
|
||||
<strong>Modem Connection</strong>
|
||||
<div class="small text-muted" id="test_modem_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_modem_status" class="badge bg-secondary">Pending</span>
|
||||
</div>
|
||||
|
||||
<!-- Test: SIM Card -->
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_sim">
|
||||
<div>
|
||||
<strong>SIM Card</strong>
|
||||
<div class="small text-muted" id="test_sim_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_sim_status" class="badge bg-secondary">Pending</span>
|
||||
</div>
|
||||
|
||||
<!-- Test: Signal Strength -->
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_signal">
|
||||
<div>
|
||||
<strong>Signal Strength</strong>
|
||||
<div class="small text-muted" id="test_signal_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_signal_status" class="badge bg-secondary">Pending</span>
|
||||
</div>
|
||||
|
||||
<!-- Test: Network Connection -->
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center" id="test_network">
|
||||
<div>
|
||||
<strong>Network Connection</strong>
|
||||
<div class="small text-muted" id="test_network_detail">Waiting...</div>
|
||||
</div>
|
||||
<span id="test_network_status" class="badge bg-secondary">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 detailed logs
|
||||
</button>
|
||||
<div class="collapse mt-2" id="selftest_logs_collapse">
|
||||
<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-primary" id="selfTestCopyBtn" onclick="openShareReportModal()" disabled>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-share me-1" viewBox="0 0 16 16">
|
||||
<path d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/>
|
||||
</svg>
|
||||
Share Report
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="selfTestDoneBtn" disabled>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Report Modal -->
|
||||
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="shareReportModalLabel">Share Diagnostic Report</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>Need help?</strong> You can send this diagnostic report to our support team at
|
||||
<a href="mailto:contact@aircarto.fr?subject=NebuleAir%20Diagnostic%20Report" class="alert-link">contact@aircarto.fr</a>
|
||||
<br><small>Select all the text below (Ctrl+A) and copy it (Ctrl+C), or use the Download button.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<textarea id="shareReportText" class="form-control font-monospace" rows="15" readonly style="font-size: 0.75rem; background-color: #1e1e1e; color: #d4d4d4;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" onclick="downloadReport()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
Download (.txt)
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="selectAllReportText()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cursor-text me-1" viewBox="0 0 16 16">
|
||||
<path d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2z"/>
|
||||
</svg>
|
||||
Select All
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +63,14 @@
|
||||
de mesurer certaines variables environnementales. La mesure
|
||||
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
||||
</p>
|
||||
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
||||
</svg>
|
||||
Run Self Test
|
||||
</button>
|
||||
|
||||
<div class="row mb-3" id="card-container"></div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -77,6 +85,7 @@
|
||||
<!-- i18n translation system -->
|
||||
<script src="assets/js/i18n.js"></script>
|
||||
<script src="assets/js/topbar-logo.js"></script>
|
||||
<script src="assets/js/selftest.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
Reference in New Issue
Block a user