- db cache dump on diagnostics request. - individual device logs, db and files. -Device logs api endpoints and diagnostics UI. Fix: - slmm standalone now uses local TZ (was UTC only before) - fixed measurement start time logic.
981 lines
38 KiB
HTML
981 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>SLMM NL43 Standalone</title>
|
|
<style>
|
|
body { font-family: system-ui, -apple-system, sans-serif; margin: 24px; max-width: 900px; }
|
|
fieldset { margin-bottom: 16px; padding: 12px; }
|
|
legend { font-weight: 600; }
|
|
label { display: block; margin-bottom: 6px; font-weight: 600; }
|
|
input { width: 100%; padding: 8px; margin-bottom: 10px; }
|
|
button { padding: 8px 12px; margin-right: 8px; }
|
|
#log { background: #f6f8fa; border: 1px solid #d0d7de; padding: 12px; min-height: 120px; white-space: pre-wrap; }
|
|
.diagnostic-item { margin: 8px 0; padding: 8px; border-left: 4px solid #888; background: #f6f8fa; }
|
|
.diagnostic-item.pass { border-left-color: #0a0; }
|
|
.diagnostic-item.fail { border-left-color: #d00; }
|
|
.diagnostic-item.warning { border-left-color: #fa0; }
|
|
.diagnostic-item.skip { border-left-color: #888; }
|
|
.diagnostic-status { font-weight: 600; margin-right: 8px; text-transform: uppercase; font-size: 0.85em; }
|
|
.diagnostic-status.pass { color: #0a0; }
|
|
.diagnostic-status.fail { color: #d00; }
|
|
.diagnostic-status.warning { color: #fa0; }
|
|
.diagnostic-status.skip { color: #888; }
|
|
#diagnosticsSummary { font-size: 1.1em; font-weight: 600; margin-bottom: 12px; padding: 8px; border-radius: 4px; }
|
|
#diagnosticsSummary.pass { background: #d4edda; color: #155724; }
|
|
#diagnosticsSummary.fail { background: #f8d7da; color: #721c24; }
|
|
#diagnosticsSummary.degraded { background: #fff3cd; color: #856404; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>SLMM NL43 Standalone</h1>
|
|
<p>Configure a unit (host/port), then use controls to Start/Stop and fetch live status.</p>
|
|
<p style="margin-bottom: 16px;">
|
|
<a href="/roster" style="color: #0969da; text-decoration: none; font-weight: 600;">📊 View Device Roster</a>
|
|
<span style="margin: 0 8px; color: #d0d7de;">|</span>
|
|
<a href="/docs" style="color: #0969da; text-decoration: none;">API Documentation</a>
|
|
</p>
|
|
|
|
<fieldset>
|
|
<legend>🔍 Connection Diagnostics</legend>
|
|
<button onclick="runDiagnostics()">Run Diagnostics</button>
|
|
<button onclick="clearDiagnostics()">Clear</button>
|
|
<div id="diagnosticsResults" style="margin-top: 12px;"></div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Unit Selection & Config</legend>
|
|
|
|
<div style="display: flex; gap: 8px; align-items: flex-end; margin-bottom: 12px;">
|
|
<div style="flex: 1;">
|
|
<label>Select Device</label>
|
|
<select id="deviceSelector" onchange="loadSelectedDevice()" style="width: 100%; padding: 8px; margin-bottom: 0;">
|
|
<option value="">-- Select a device --</option>
|
|
</select>
|
|
</div>
|
|
<button onclick="refreshDeviceList()" style="padding: 8px 12px;">↻ Refresh</button>
|
|
</div>
|
|
|
|
<div style="padding: 12px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px; margin-bottom: 12px;">
|
|
<div style="display: flex; gap: 16px;">
|
|
<div style="flex: 1;">
|
|
<label>Unit ID</label>
|
|
<input id="unitId" value="nl43-1" />
|
|
</div>
|
|
<div style="flex: 2;">
|
|
<label>Host</label>
|
|
<input id="host" value="127.0.0.1" />
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<label>TCP Port</label>
|
|
<input id="port" type="number" value="2255" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin: 12px 0;">
|
|
<label style="display: inline-flex; align-items: center; margin-right: 16px;">
|
|
<input type="checkbox" id="tcpEnabled" checked style="width: auto; margin-right: 6px;" />
|
|
TCP Enabled
|
|
</label>
|
|
<label style="display: inline-flex; align-items: center;">
|
|
<input type="checkbox" id="ftpEnabled" style="width: auto; margin-right: 6px;" />
|
|
FTP Enabled
|
|
</label>
|
|
</div>
|
|
|
|
<div id="ftpCredentials" style="display: none; margin-top: 12px; padding: 12px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px;">
|
|
<label>FTP Username</label>
|
|
<input id="ftpUsername" value="USER" />
|
|
<label>FTP Password</label>
|
|
<input id="ftpPassword" type="password" value="0000" />
|
|
</div>
|
|
|
|
<div style="margin-top: 12px;">
|
|
<button onclick="saveConfig()">Save Config</button>
|
|
<button onclick="loadConfig()">Load Config</button>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Measurement Controls</legend>
|
|
<button onclick="start()">Start</button>
|
|
<button onclick="stop()">Stop</button>
|
|
<button onclick="pause()">Pause</button>
|
|
<button onclick="resume()">Resume</button>
|
|
<button onclick="reset()">Reset</button>
|
|
<button onclick="store()">Store Data</button>
|
|
<button onclick="live()">Fetch Live (DOD)</button>
|
|
<button onclick="getResults()">Get Results (DLC)</button>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Power Management</legend>
|
|
<button onclick="sleepDevice()">Sleep</button>
|
|
<button onclick="wakeDevice()">Wake</button>
|
|
<button onclick="getSleepStatus()">Check Sleep Status</button>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Device Info</legend>
|
|
<button onclick="getBattery()">Get Battery</button>
|
|
<button onclick="getClock()">Get Clock</button>
|
|
<button onclick="syncClock()">Sync Clock to PC</button>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Measurement Settings</legend>
|
|
<button onclick="getAllSettings()" style="margin-bottom: 12px; font-weight: bold;">Get ALL Settings</button>
|
|
<div style="margin-bottom: 8px;">
|
|
<label style="display: inline; margin-right: 8px;">Frequency Weighting:</label>
|
|
<button onclick="getFreqWeighting()">Get</button>
|
|
<button onclick="setFreqWeighting('A')">Set A</button>
|
|
<button onclick="setFreqWeighting('C')">Set C</button>
|
|
<button onclick="setFreqWeighting('Z')">Set Z</button>
|
|
</div>
|
|
<div>
|
|
<label style="display: inline; margin-right: 8px;">Time Weighting:</label>
|
|
<button onclick="getTimeWeighting()">Get</button>
|
|
<button onclick="setTimeWeighting('F')">Set Fast</button>
|
|
<button onclick="setTimeWeighting('S')">Set Slow</button>
|
|
<button onclick="setTimeWeighting('I')">Set Impulse</button>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Live Stream (DRD)</legend>
|
|
<button id="streamBtn" onclick="toggleStream()">Start Stream</button>
|
|
<span id="streamStatus" style="margin-left: 12px; color: #888;">Not connected</span>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>FTP File Download</legend>
|
|
<button onclick="enableFTP()">Enable FTP</button>
|
|
<button onclick="disableFTP()">Disable FTP</button>
|
|
<button onclick="checkFTPStatus()">Check FTP Status</button>
|
|
<button onclick="listFiles()">List Files</button>
|
|
<div id="fileList" style="margin-top: 12px; max-height: 200px; overflow-y: auto; background: #f6f8fa; border: 1px solid #d0d7de; padding: 8px;"></div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Status</legend>
|
|
<pre id="status">No data yet.</pre>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Log</legend>
|
|
<div id="log"></div>
|
|
</fieldset>
|
|
|
|
<script>
|
|
const logEl = document.getElementById('log');
|
|
const statusEl = document.getElementById('status');
|
|
const streamBtn = document.getElementById('streamBtn');
|
|
const streamStatus = document.getElementById('streamStatus');
|
|
|
|
let ws = null;
|
|
let streamUpdateCount = 0;
|
|
let availableDevices = [];
|
|
|
|
function log(msg) {
|
|
logEl.textContent += msg + "\n";
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
function toggleFtpCredentials() {
|
|
const ftpEnabled = document.getElementById('ftpEnabled').checked;
|
|
const ftpCredentials = document.getElementById('ftpCredentials');
|
|
ftpCredentials.style.display = ftpEnabled ? 'block' : 'none';
|
|
}
|
|
|
|
// Load device list from roster
|
|
async function refreshDeviceList() {
|
|
try {
|
|
const res = await fetch('/api/nl43/roster');
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
log('Failed to load device list');
|
|
return;
|
|
}
|
|
|
|
availableDevices = data.devices || [];
|
|
const selector = document.getElementById('deviceSelector');
|
|
|
|
// Save current selection
|
|
const currentSelection = selector.value;
|
|
|
|
// Clear and rebuild options
|
|
selector.innerHTML = '<option value="">-- Select a device --</option>';
|
|
|
|
availableDevices.forEach(device => {
|
|
const option = document.createElement('option');
|
|
option.value = device.unit_id;
|
|
|
|
// Add status indicator
|
|
let statusIcon = '⚪';
|
|
if (device.status) {
|
|
if (device.status.is_reachable === false) {
|
|
statusIcon = '🔴';
|
|
} else if (device.status.last_success) {
|
|
const lastSeen = new Date(device.status.last_success);
|
|
const ageMinutes = Math.floor((Date.now() - lastSeen) / 60000);
|
|
statusIcon = ageMinutes < 5 ? '🟢' : '🟡';
|
|
}
|
|
}
|
|
|
|
option.textContent = `${statusIcon} ${device.unit_id} (${device.host})`;
|
|
selector.appendChild(option);
|
|
});
|
|
|
|
// Restore selection if it still exists
|
|
if (currentSelection && availableDevices.find(d => d.unit_id === currentSelection)) {
|
|
selector.value = currentSelection;
|
|
}
|
|
|
|
log(`Loaded ${availableDevices.length} device(s) from roster`);
|
|
} catch (err) {
|
|
log(`Error loading device list: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Load selected device configuration
|
|
function loadSelectedDevice() {
|
|
const selector = document.getElementById('deviceSelector');
|
|
const unitId = selector.value;
|
|
|
|
if (!unitId) {
|
|
return;
|
|
}
|
|
|
|
const device = availableDevices.find(d => d.unit_id === unitId);
|
|
if (!device) {
|
|
log(`Device ${unitId} not found in list`);
|
|
return;
|
|
}
|
|
|
|
// Populate form fields
|
|
document.getElementById('unitId').value = device.unit_id;
|
|
document.getElementById('host').value = device.host;
|
|
document.getElementById('port').value = device.tcp_port || 2255;
|
|
document.getElementById('tcpEnabled').checked = device.tcp_enabled || false;
|
|
document.getElementById('ftpEnabled').checked = device.ftp_enabled || false;
|
|
|
|
if (device.ftp_username) {
|
|
document.getElementById('ftpUsername').value = device.ftp_username;
|
|
}
|
|
if (device.ftp_password) {
|
|
document.getElementById('ftpPassword').value = device.ftp_password;
|
|
}
|
|
|
|
toggleFtpCredentials();
|
|
|
|
log(`Loaded configuration for ${device.unit_id}`);
|
|
}
|
|
|
|
// Add event listeners
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.getElementById('ftpEnabled').addEventListener('change', toggleFtpCredentials);
|
|
|
|
// Load device list on page load
|
|
refreshDeviceList();
|
|
});
|
|
|
|
async function runDiagnostics() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const resultsEl = document.getElementById('diagnosticsResults');
|
|
|
|
resultsEl.innerHTML = '<p style="color: #888;">Running diagnostics...</p>';
|
|
log('Running diagnostics...');
|
|
|
|
try {
|
|
const res = await fetch(`/api/nl43/${unitId}/diagnostics`);
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
resultsEl.innerHTML = `<p style="color: #d00;">Diagnostics failed: ${data.detail || 'Unknown error'}</p>`;
|
|
log(`Diagnostics failed: ${res.status}`);
|
|
return;
|
|
}
|
|
|
|
// Build results HTML
|
|
let html = '';
|
|
|
|
// Overall status summary
|
|
const statusText = {
|
|
'pass': '✓ All systems operational',
|
|
'fail': '✗ Connection failed',
|
|
'degraded': '⚠ Partial connectivity'
|
|
};
|
|
|
|
html += `<div id="diagnosticsSummary" class="${data.overall_status}">`;
|
|
html += statusText[data.overall_status] || data.overall_status;
|
|
html += `</div>`;
|
|
|
|
// Individual test results
|
|
const testNames = {
|
|
'config_exists': '📋 Configuration',
|
|
'tcp_enabled': '🔌 TCP Enabled',
|
|
'modem_reachable': '📡 Modem/Router Reachable',
|
|
'tcp_connection': '🌐 Device Port Connection',
|
|
'command_response': '💬 Device Response',
|
|
'ftp_status': '📁 FTP Status'
|
|
};
|
|
|
|
for (const [testKey, testResult] of Object.entries(data.tests)) {
|
|
const testName = testNames[testKey] || testKey;
|
|
html += `<div class="diagnostic-item ${testResult.status}">`;
|
|
html += `<span class="diagnostic-status ${testResult.status}">${testResult.status}</span>`;
|
|
html += `<strong>${testName}:</strong> ${testResult.message}`;
|
|
html += `</div>`;
|
|
}
|
|
|
|
html += `<p style="margin-top: 12px; font-size: 0.9em; color: #666;">Last run: ${new Date(data.timestamp).toLocaleString()}</p>`;
|
|
|
|
// Add database dump section if available
|
|
if (data.database_dump) {
|
|
html += `<div style="margin-top: 16px; border-top: 1px solid #d0d7de; padding-top: 12px;">`;
|
|
html += `<h4 style="margin: 0 0 12px 0;">📦 Database Dump</h4>`;
|
|
|
|
// Config section
|
|
if (data.database_dump.config) {
|
|
const cfg = data.database_dump.config;
|
|
html += `<div style="background: #f0f4f8; padding: 12px; border-radius: 4px; margin-bottom: 12px;">`;
|
|
html += `<strong>Configuration (nl43_config)</strong>`;
|
|
html += `<table style="width: 100%; margin-top: 8px; font-size: 0.9em;">`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Host</td><td>${cfg.host}:${cfg.tcp_port}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">TCP Enabled</td><td>${cfg.tcp_enabled ? '✓' : '✗'}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">FTP Enabled</td><td>${cfg.ftp_enabled ? '✓' : '✗'}${cfg.ftp_enabled ? ` (port ${cfg.ftp_port}, user: ${cfg.ftp_username || 'none'})` : ''}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Background Polling</td><td>${cfg.poll_enabled ? `✓ every ${cfg.poll_interval_seconds}s` : '✗ disabled'}</td></tr>`;
|
|
html += `</table></div>`;
|
|
}
|
|
|
|
// Status cache section
|
|
if (data.database_dump.status_cache) {
|
|
const cache = data.database_dump.status_cache;
|
|
html += `<div style="background: #f0f8f4; padding: 12px; border-radius: 4px; margin-bottom: 12px;">`;
|
|
html += `<strong>Status Cache (nl43_status)</strong>`;
|
|
html += `<table style="width: 100%; margin-top: 8px; font-size: 0.9em;">`;
|
|
|
|
// Measurement state and timing
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Measurement State</td><td><strong>${cache.measurement_state || 'unknown'}</strong></td></tr>`;
|
|
if (cache.measurement_start_time) {
|
|
const startTime = new Date(cache.measurement_start_time);
|
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
const elapsedStr = elapsed > 3600 ? `${Math.floor(elapsed/3600)}h ${Math.floor((elapsed%3600)/60)}m` : elapsed > 60 ? `${Math.floor(elapsed/60)}m ${elapsed%60}s` : `${elapsed}s`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Measurement Started</td><td>${startTime.toLocaleString()} (${elapsedStr} ago)</td></tr>`;
|
|
}
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Counter (d0)</td><td>${cache.counter || 'N/A'}</td></tr>`;
|
|
|
|
// Sound levels
|
|
html += `<tr><td colspan="2" style="padding: 8px 8px 2px 8px; font-weight: 600; border-top: 1px solid #d0d7de;">Sound Levels (dB)</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Lp (Instantaneous)</td><td>${cache.lp || 'N/A'}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Leq (Equivalent)</td><td>${cache.leq || 'N/A'}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Lmax / Lmin</td><td>${cache.lmax || 'N/A'} / ${cache.lmin || 'N/A'}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Lpeak</td><td>${cache.lpeak || 'N/A'}</td></tr>`;
|
|
|
|
// Device status
|
|
html += `<tr><td colspan="2" style="padding: 8px 8px 2px 8px; font-weight: 600; border-top: 1px solid #d0d7de;">Device Status</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Battery</td><td>${cache.battery_level || 'N/A'}${cache.power_source ? ` (${cache.power_source})` : ''}</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">SD Card</td><td>${cache.sd_remaining_mb ? `${cache.sd_remaining_mb} MB` : 'N/A'}${cache.sd_free_ratio ? ` (${cache.sd_free_ratio} free)` : ''}</td></tr>`;
|
|
|
|
// Polling status
|
|
html += `<tr><td colspan="2" style="padding: 8px 8px 2px 8px; font-weight: 600; border-top: 1px solid #d0d7de;">Polling Status</td></tr>`;
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Reachable</td><td>${cache.is_reachable ? '🟢 Yes' : '🔴 No'}</td></tr>`;
|
|
if (cache.last_seen) {
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Last Seen</td><td>${new Date(cache.last_seen).toLocaleString()}</td></tr>`;
|
|
}
|
|
if (cache.last_success) {
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Last Success</td><td>${new Date(cache.last_success).toLocaleString()}</td></tr>`;
|
|
}
|
|
if (cache.last_poll_attempt) {
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Last Poll Attempt</td><td>${new Date(cache.last_poll_attempt).toLocaleString()}</td></tr>`;
|
|
}
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Consecutive Failures</td><td>${cache.consecutive_failures || 0}</td></tr>`;
|
|
if (cache.last_error) {
|
|
html += `<tr><td style="padding: 2px 8px; color: #666;">Last Error</td><td style="color: #d00; font-size: 0.85em;">${cache.last_error}</td></tr>`;
|
|
}
|
|
|
|
html += `</table></div>`;
|
|
|
|
// Raw payload (collapsible)
|
|
if (cache.raw_payload) {
|
|
html += `<details style="margin-top: 8px;"><summary style="cursor: pointer; color: #666; font-size: 0.9em;">📄 Raw Payload</summary>`;
|
|
html += `<pre style="background: #f6f8fa; padding: 8px; border-radius: 4px; font-size: 0.8em; overflow-x: auto; margin-top: 8px;">${cache.raw_payload}</pre></details>`;
|
|
}
|
|
} else {
|
|
html += `<p style="color: #888; font-style: italic;">No cached status available for this unit.</p>`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
}
|
|
|
|
// Fetch and display device logs
|
|
try {
|
|
const logsRes = await fetch(`/api/nl43/${unitId}/logs?limit=50`);
|
|
if (logsRes.ok) {
|
|
const logsData = await logsRes.json();
|
|
if (logsData.logs && logsData.logs.length > 0) {
|
|
html += `<div style="margin-top: 16px; border-top: 1px solid #d0d7de; padding-top: 12px;">`;
|
|
html += `<h4 style="margin: 0 0 12px 0;">📋 Device Logs (${logsData.stats.total} total)</h4>`;
|
|
|
|
// Stats summary
|
|
if (logsData.stats.by_level) {
|
|
html += `<div style="margin-bottom: 8px; font-size: 0.85em; color: #666;">`;
|
|
const levels = logsData.stats.by_level;
|
|
const parts = [];
|
|
if (levels.ERROR) parts.push(`<span style="color: #d00;">${levels.ERROR} errors</span>`);
|
|
if (levels.WARNING) parts.push(`<span style="color: #fa0;">${levels.WARNING} warnings</span>`);
|
|
if (levels.INFO) parts.push(`${levels.INFO} info`);
|
|
html += parts.join(' · ');
|
|
html += `</div>`;
|
|
}
|
|
|
|
// Log entries (collapsible)
|
|
html += `<details open><summary style="cursor: pointer; font-size: 0.9em; margin-bottom: 8px;">Recent entries (${logsData.logs.length})</summary>`;
|
|
html += `<div style="max-height: 300px; overflow-y: auto; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; font-size: 0.8em; font-family: monospace;">`;
|
|
|
|
logsData.logs.forEach(entry => {
|
|
const levelColor = {
|
|
'ERROR': '#d00',
|
|
'WARNING': '#b86e00',
|
|
'INFO': '#0969da',
|
|
'DEBUG': '#888'
|
|
}[entry.level] || '#666';
|
|
|
|
const time = new Date(entry.timestamp).toLocaleString();
|
|
html += `<div style="margin-bottom: 4px; border-bottom: 1px solid #eee; padding-bottom: 4px;">`;
|
|
html += `<span style="color: #888;">${time}</span> `;
|
|
html += `<span style="color: ${levelColor}; font-weight: 600;">[${entry.level}]</span> `;
|
|
html += `<span style="color: #666;">[${entry.category}]</span> `;
|
|
html += `${entry.message}`;
|
|
html += `</div>`;
|
|
});
|
|
|
|
html += `</div></details>`;
|
|
html += `</div>`;
|
|
}
|
|
}
|
|
} catch (logErr) {
|
|
console.log('Could not fetch device logs:', logErr);
|
|
}
|
|
|
|
resultsEl.innerHTML = html;
|
|
log(`Diagnostics complete: ${data.overall_status}`);
|
|
|
|
} catch (err) {
|
|
resultsEl.innerHTML = `<p style="color: #d00;">Error running diagnostics: ${err.message}</p>`;
|
|
log(`Diagnostics error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function clearDiagnostics() {
|
|
document.getElementById('diagnosticsResults').innerHTML = '';
|
|
log('Diagnostics cleared');
|
|
}
|
|
|
|
async function saveConfig() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const host = document.getElementById('host').value;
|
|
const port = parseInt(document.getElementById('port').value, 10);
|
|
const tcpEnabled = document.getElementById('tcpEnabled').checked;
|
|
const ftpEnabled = document.getElementById('ftpEnabled').checked;
|
|
const ftpUsername = document.getElementById('ftpUsername').value;
|
|
const ftpPassword = document.getElementById('ftpPassword').value;
|
|
|
|
const config = {
|
|
host,
|
|
tcp_port: port,
|
|
tcp_enabled: tcpEnabled,
|
|
ftp_enabled: ftpEnabled
|
|
};
|
|
|
|
// Only include FTP credentials if FTP is enabled
|
|
if (ftpEnabled) {
|
|
config.ftp_username = ftpUsername;
|
|
config.ftp_password = ftpPassword;
|
|
}
|
|
|
|
const res = await fetch(`/api/nl43/${unitId}/config`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
const data = await res.json();
|
|
log(`Saved config: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function loadConfig() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/config`);
|
|
if (!res.ok) {
|
|
log(`Load config failed: ${res.status}`);
|
|
return;
|
|
}
|
|
const response = await res.json();
|
|
const data = response.data;
|
|
document.getElementById('host').value = data.host;
|
|
document.getElementById('port').value = data.tcp_port;
|
|
document.getElementById('tcpEnabled').checked = data.tcp_enabled || false;
|
|
document.getElementById('ftpEnabled').checked = data.ftp_enabled || false;
|
|
|
|
// Show/hide FTP credentials based on FTP enabled status
|
|
toggleFtpCredentials();
|
|
|
|
log(`Loaded config: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function start() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/start`, { method: 'POST' });
|
|
log(`Start: ${res.status}`);
|
|
}
|
|
|
|
async function stop() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/stop`, { method: 'POST' });
|
|
log(`Stop: ${res.status}`);
|
|
}
|
|
|
|
async function store() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/store`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Store: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function live() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/live`);
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
log(`Live failed: ${res.status} ${JSON.stringify(data)}`);
|
|
return;
|
|
}
|
|
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
|
log(`Live: ${JSON.stringify(data.data)}`);
|
|
}
|
|
|
|
async function getResults() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/results`);
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
log(`Get Results failed: ${res.status} ${JSON.stringify(data)}`);
|
|
return;
|
|
}
|
|
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
|
log(`Results (DLC): Retrieved final calculation data`);
|
|
}
|
|
|
|
// New measurement control functions
|
|
async function pause() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/pause`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Pause: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function resume() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/resume`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Resume: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function reset() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/reset`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Reset: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
// Power management functions
|
|
async function sleepDevice() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/sleep`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Device entering sleep mode`);
|
|
} else {
|
|
log(`Sleep failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function wakeDevice() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/wake`, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Device waking from sleep mode`);
|
|
} else {
|
|
log(`Wake failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function getSleepStatus() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/sleep/status`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Sleep Status: ${data.sleep_status}`);
|
|
} else {
|
|
log(`Get Sleep Status failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
// Device info functions
|
|
async function getBattery() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/battery`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Battery Level: ${data.battery_level}%`);
|
|
} else {
|
|
log(`Get Battery failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function getClock() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/clock`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Device Clock: ${data.clock}`);
|
|
} else {
|
|
log(`Get Clock failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function syncClock() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const now = new Date();
|
|
const datetime = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')},${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
|
|
const res = await fetch(`/api/nl43/${unitId}/clock`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ datetime })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Clock synced to: ${datetime}`);
|
|
} else {
|
|
log(`Sync Clock failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
// Measurement settings functions
|
|
async function getAllSettings() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
log('Retrieving all device settings (this may take 10-15 seconds)...');
|
|
|
|
const res = await fetch(`/api/nl43/${unitId}/settings`);
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
log(`Get All Settings failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
|
|
return;
|
|
}
|
|
|
|
// Display in status area
|
|
statusEl.textContent = JSON.stringify(data.settings, null, 2);
|
|
|
|
// Log summary
|
|
log('=== ALL DEVICE SETTINGS ===');
|
|
Object.entries(data.settings).forEach(([key, value]) => {
|
|
log(`${key}: ${value}`);
|
|
});
|
|
log('===========================');
|
|
}
|
|
|
|
async function getFreqWeighting() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Frequency Weighting (Main): ${data.frequency_weighting}`);
|
|
} else {
|
|
log(`Get Freq Weighting failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function setFreqWeighting(weighting) {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ weighting, channel: 'Main' })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Frequency Weighting set to: ${weighting}`);
|
|
} else {
|
|
log(`Set Freq Weighting failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function getTimeWeighting() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/time-weighting?channel=Main`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Time Weighting (Main): ${data.time_weighting}`);
|
|
} else {
|
|
log(`Get Time Weighting failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function setTimeWeighting(weighting) {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/time-weighting`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ weighting, channel: 'Main' })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`Time Weighting set to: ${weighting}`);
|
|
} else {
|
|
log(`Set Time Weighting failed: ${res.status} - ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
function toggleStream() {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
stopStream();
|
|
} else {
|
|
startStream();
|
|
}
|
|
}
|
|
|
|
function startStream() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${wsProtocol}//${window.location.host}/api/nl43/${unitId}/stream`;
|
|
|
|
log(`Connecting to WebSocket: ${wsUrl}`);
|
|
streamUpdateCount = 0;
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
log('WebSocket connected - DRD streaming started');
|
|
streamBtn.textContent = 'Stop Stream';
|
|
streamStatus.textContent = 'Connected';
|
|
streamStatus.style.color = '#0a0';
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (data.error) {
|
|
log(`Stream error: ${data.error} - ${data.detail || ''}`);
|
|
return;
|
|
}
|
|
|
|
streamUpdateCount++;
|
|
|
|
// Update status display with live data
|
|
const displayData = {
|
|
unit_id: data.unit_id,
|
|
timestamp: data.timestamp,
|
|
lp: data.lp,
|
|
leq: data.leq,
|
|
lmax: data.lmax,
|
|
lmin: data.lmin,
|
|
lpeak: data.lpeak
|
|
};
|
|
statusEl.textContent = JSON.stringify(displayData, null, 2);
|
|
|
|
// Log every 10th update to avoid spamming
|
|
if (streamUpdateCount % 10 === 0) {
|
|
log(`Stream update #${streamUpdateCount}: Lp=${data.lp} Leq=${data.leq} Lpeak=${data.lpeak}`);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
log('WebSocket error occurred');
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
log(`WebSocket closed (received ${streamUpdateCount} updates)`);
|
|
streamBtn.textContent = 'Start Stream';
|
|
streamStatus.textContent = 'Not connected';
|
|
streamStatus.style.color = '#888';
|
|
ws = null;
|
|
};
|
|
}
|
|
|
|
function stopStream() {
|
|
if (ws) {
|
|
log('Closing WebSocket...');
|
|
ws.close();
|
|
}
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (ws) ws.close();
|
|
});
|
|
|
|
// FTP Functions
|
|
async function enableFTP() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/ftp/enable`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Enable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function disableFTP() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/ftp/disable`, { method: 'POST' });
|
|
const data = await res.json();
|
|
log(`Disable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
|
}
|
|
|
|
async function checkFTPStatus() {
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/ftp/status`);
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
log(`FTP Status: ${data.ftp_status} (enabled: ${data.ftp_enabled})`);
|
|
} else {
|
|
log(`FTP Status check failed: ${res.status}`);
|
|
}
|
|
}
|
|
|
|
let currentPath = '/';
|
|
|
|
async function listFiles(path = '/') {
|
|
currentPath = path;
|
|
const unitId = document.getElementById('unitId').value;
|
|
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
log(`List files failed: ${res.status} ${JSON.stringify(data)}`);
|
|
return;
|
|
}
|
|
|
|
const fileListEl = document.getElementById('fileList');
|
|
fileListEl.innerHTML = '';
|
|
|
|
// Add breadcrumb navigation
|
|
const breadcrumb = document.createElement('div');
|
|
breadcrumb.style.marginBottom = '8px';
|
|
breadcrumb.style.padding = '4px';
|
|
breadcrumb.style.background = '#e1e4e8';
|
|
breadcrumb.style.borderRadius = '3px';
|
|
breadcrumb.innerHTML = '<strong>Path:</strong> ';
|
|
|
|
const pathParts = path.split('/').filter(p => p);
|
|
let builtPath = '/';
|
|
|
|
// Root link
|
|
const rootLink = document.createElement('a');
|
|
rootLink.href = '#';
|
|
rootLink.textContent = '/';
|
|
rootLink.style.marginRight = '4px';
|
|
rootLink.onclick = (e) => { e.preventDefault(); listFiles('/'); };
|
|
breadcrumb.appendChild(rootLink);
|
|
|
|
// Path component links
|
|
pathParts.forEach((part, idx) => {
|
|
builtPath += part + '/';
|
|
const linkPath = builtPath;
|
|
|
|
const separator = document.createElement('span');
|
|
separator.textContent = ' / ';
|
|
breadcrumb.appendChild(separator);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.textContent = part;
|
|
link.style.marginRight = '4px';
|
|
if (idx === pathParts.length - 1) {
|
|
link.style.fontWeight = 'bold';
|
|
link.style.color = '#000';
|
|
}
|
|
link.onclick = (e) => { e.preventDefault(); listFiles(linkPath); };
|
|
breadcrumb.appendChild(link);
|
|
});
|
|
|
|
fileListEl.appendChild(breadcrumb);
|
|
|
|
if (data.files.length === 0) {
|
|
const emptyDiv = document.createElement('div');
|
|
emptyDiv.textContent = 'No files found';
|
|
emptyDiv.style.padding = '8px';
|
|
fileListEl.appendChild(emptyDiv);
|
|
log(`No files found in ${path}`);
|
|
return;
|
|
}
|
|
|
|
log(`Found ${data.count} files in ${path}`);
|
|
|
|
data.files.forEach(file => {
|
|
const fileDiv = document.createElement('div');
|
|
fileDiv.style.marginBottom = '8px';
|
|
fileDiv.style.padding = '4px';
|
|
fileDiv.style.borderBottom = '1px solid #ddd';
|
|
|
|
const icon = file.is_dir ? '📁' : '📄';
|
|
const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`;
|
|
|
|
if (file.is_dir) {
|
|
fileDiv.innerHTML = `
|
|
${icon} <a href="#" onclick="event.preventDefault(); listFiles('${file.path}');" style="font-weight: bold;">${file.name}</a>
|
|
<br><small style="color: #666;">${file.path}</small>
|
|
`;
|
|
} else {
|
|
fileDiv.innerHTML = `
|
|
${icon} <strong>${file.name}</strong>${size}
|
|
<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>
|
|
<br><small style="color: #666;">${file.path}</small>
|
|
`;
|
|
}
|
|
|
|
fileListEl.appendChild(fileDiv);
|
|
});
|
|
}
|
|
|
|
async function downloadFile(remotePath) {
|
|
const unitId = document.getElementById('unitId').value;
|
|
log(`Downloading file: ${remotePath}...`);
|
|
|
|
try {
|
|
const res = await fetch(`/api/nl43/${unitId}/ftp/download`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ remote_path: remotePath })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
log(`Download failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
|
|
return;
|
|
}
|
|
|
|
// Trigger browser download
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = remotePath.split('/').pop();
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
log(`Downloaded: ${remotePath}`);
|
|
} catch (error) {
|
|
log(`Download error: ${error.message}`);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|