feat: Replace Settings with Device Info modal

- Rename Settings to Device Info with new cpu icon
- Display device parameters in readable table format
- Add copy-to-clipboard buttons for Name and Public Key
- Add map button for device location coordinates
- Show human-readable parameter names and values
- Hide telemetry parameters (not commonly needed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-18 17:02:42 +01:00
parent 9bc27042da
commit b5cdce18f2
2 changed files with 106 additions and 19 deletions

View File

@@ -425,9 +425,9 @@ function setupEventListeners() {
isUserScrolling = !isAtBottom;
});
// Load device info when settings modal opens
const settingsModal = document.getElementById('settingsModal');
settingsModal.addEventListener('show.bs.modal', function() {
// Load device info when modal opens
const deviceInfoModal = document.getElementById('deviceInfoModal');
deviceInfoModal.addEventListener('show.bs.modal', function() {
loadDeviceInfo();
});
@@ -767,24 +767,110 @@ async function loadStatus() {
}
}
/**
* Copy text to clipboard with visual feedback
*/
async function copyToClipboard(text, btnElement) {
try {
await navigator.clipboard.writeText(text);
const icon = btnElement.querySelector('i');
const originalClass = icon.className;
icon.className = 'bi bi-check';
setTimeout(() => { icon.className = originalClass; }, 1500);
} catch (err) {
console.error('Failed to copy:', err);
}
}
/**
* Load device information
*/
async function loadDeviceInfo() {
const infoEl = document.getElementById('deviceInfo');
infoEl.innerHTML = '<div class="spinner-border spinner-border-sm"></div> Loading...';
const container = document.getElementById('deviceInfoContent');
container.innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm"></div> Loading...</div>';
try {
const response = await fetch('/api/device/info');
const data = await response.json();
if (data.success) {
infoEl.innerHTML = `<pre class="mb-0">${escapeHtml(data.info)}</pre>`;
} else {
infoEl.innerHTML = `<span class="text-danger">Error: ${escapeHtml(data.error)}</span>`;
if (!data.success) {
container.innerHTML = `<div class="alert alert-danger mb-0">${escapeHtml(data.error)}</div>`;
return;
}
// Parse JSON from the info string
let info;
try {
// Extract JSON part (skip the header lines like "MarWoj|*...")
const jsonMatch = data.info.match(/\{[\s\S]*\}/);
info = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
} catch (e) {
container.innerHTML = `<pre class="mb-0 small">${escapeHtml(data.info)}</pre>`;
return;
}
if (!info) {
container.innerHTML = `<pre class="mb-0 small">${escapeHtml(data.info)}</pre>`;
return;
}
// Type mapping
const typeNames = { 1: 'Companion', 2: 'Repeater', 3: 'Room Server', 4: 'Sensor' };
const typeName = typeNames[info.adv_type] || `Unknown (${info.adv_type})`;
// Shorten public key for display
const pubKey = info.public_key || '';
const shortKey = pubKey.length > 12 ? `${pubKey.slice(0, 6)}...${pubKey.slice(-6)}` : pubKey;
// Location
const hasLocation = info.adv_lat && info.adv_lon && (info.adv_lat !== 0 || info.adv_lon !== 0);
const coords = hasLocation ? `${info.adv_lat.toFixed(6)}, ${info.adv_lon.toFixed(6)}` : 'Not available';
// Build table rows
const rows = [
{ label: 'Name', value: escapeHtml(info.name || 'Unknown'), copyValue: info.name },
{ label: 'Type', value: typeName },
{ label: 'Public Key', value: `<code class="small">${escapeHtml(shortKey)}</code>`, copyValue: pubKey },
{ label: 'Location', value: coords, showMap: hasLocation, lat: info.adv_lat, lon: info.adv_lon, name: info.name },
{ label: 'TX Power', value: `${info.tx_power || 0} / ${info.max_tx_power || 0} dBm` },
{ label: 'Frequency', value: `${info.radio_freq || 0} MHz` },
{ label: 'Bandwidth', value: `${info.radio_bw || 0} kHz` },
{ label: 'Spreading Factor', value: info.radio_sf || 0 },
{ label: 'Coding Rate', value: `4/${info.radio_cr || 0}` },
{ label: 'Multi Acks', value: info.multi_acks ? 'Enabled' : 'Disabled' },
{ label: 'Location Sharing', value: info.adv_loc_policy ? 'Enabled' : 'Disabled' },
{ label: 'Manual Add Contacts', value: info.manual_add_contacts ? 'Yes' : 'No' }
];
let html = '<table class="table table-sm mb-0">';
html += '<tbody>';
for (const row of rows) {
html += '<tr>';
html += `<td class="text-muted" style="width: 40%">${row.label}</td>`;
html += '<td>';
html += row.value;
// Copy button
if (row.copyValue) {
html += ` <button class="btn btn-link btn-sm p-0 ms-1" onclick="copyToClipboard('${escapeHtml(row.copyValue)}', this)" title="Copy to clipboard"><i class="bi bi-clipboard"></i></button>`;
}
// Map button
if (row.showMap) {
html += ` <button class="btn btn-link btn-sm p-0 ms-1" onclick="showContactOnMap('${escapeHtml(row.name)}', ${row.lat}, ${row.lon})" title="Show on map"><i class="bi bi-geo-alt"></i></button>`;
}
html += '</td>';
html += '</tr>';
}
html += '</tbody></table>';
container.innerHTML = html;
} catch (error) {
infoEl.innerHTML = '<span class="text-danger">Failed to load device info</span>';
console.error('Error loading device info:', error);
container.innerHTML = '<div class="alert alert-danger mb-0">Failed to load device info</div>';
}
}

View File

@@ -132,9 +132,9 @@
<small class="d-block text-muted">All contacts with GPS</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
<span>Settings</span>
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#deviceInfoModal" data-bs-dismiss="offcanvas">
<i class="bi bi-cpu" style="font-size: 1.5rem;"></i>
<span>Device Info</span>
</button>
</div>
</div>
@@ -260,18 +260,19 @@
</div>
</div>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1">
<!-- Device Info Modal -->
<div class="modal fade" id="deviceInfoModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-gear"></i> Settings</h5>
<h5 class="modal-title"><i class="bi bi-cpu"></i> Device Info</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6>Device Information</h6>
<div id="deviceInfo" class="small text-muted">
Loading...
<div id="deviceInfoContent">
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm"></div> Loading...
</div>
</div>
</div>
</div>