Sort order and purging fix

This commit is contained in:
Will Bradley
2025-10-12 05:14:35 -07:00
parent be243e3b50
commit 6a2148ad14
7 changed files with 219 additions and 124 deletions
+10
View File
@@ -351,6 +351,16 @@ body {
color: var(--text-secondary);
}
.message-channel {
display: inline-block;
padding: 0.125rem 0.5rem;
color: white;
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.7rem;
text-transform: uppercase;
}
/* Nodes Grid */
.nodes-grid {
display: grid;
+1 -3
View File
@@ -119,9 +119,7 @@
<option value="474572292">!1c496604</option>
</select>
<select id="message-channel">
<option value="0">LongFast (public)</option>
<option value="1">Aether (private)</option>
<option value="2">Other (2)</option>
<!-- Options populated dynamically from config -->
</select>
<button type="submit" class="btn btn-primary">Send</button>
</div>
+89 -114
View File
@@ -18,7 +18,8 @@ const state = {
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 5000, // 5 seconds
isReconnecting: false
isReconnecting: false,
channelConfig: {} // Channel name configuration
};
// Handle authentication errors (401)
@@ -189,6 +190,10 @@ const api = {
async getMqttStatus() {
return this.request('/api/mqtt/status');
},
async getChannelConfig() {
return this.request('/api/config/channels');
}
};
@@ -321,6 +326,35 @@ const ui = {
const bucketIndex = Math.min(7, Math.floor(minutesSince / bucketSize));
return colors[bucketIndex];
},
getChannelName(channelNumber) {
return state.channelConfig[channelNumber] || `Channel ${channelNumber}`;
},
// Hash a string to a color
hashStringToColor(str) {
if (!str) return '#2563eb'; // Default primary color
// Simple hash function
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // Convert to 32bit integer
}
// Convert to HSL for better color distribution
// Use hue from hash, fixed saturation and lightness for readability
const hue = Math.abs(hash % 360);
const saturation = 65; // 65% saturation for vibrant colors
const lightness = 45; // 45% lightness for good contrast with white text
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
},
getChannelColor(channelNumber) {
const channelName = this.getChannelName(channelNumber);
return this.hashStringToColor(channelName);
}
};
@@ -378,6 +412,25 @@ async function initDashboard() {
// Set user info
document.getElementById('user-info').textContent = state.user.username;
// Load channel configuration
try {
state.channelConfig = await api.getChannelConfig();
populateChannelSelector();
} catch (error) {
console.error('Error loading channel config:', error);
// Use defaults if config fails to load
state.channelConfig = {
0: 'LongFast',
1: 'Aether',
2: 'Channel 2',
3: 'Channel 3',
4: 'Channel 4',
5: 'Channel 5',
6: 'Channel 6',
7: 'Channel 7'
};
}
// Update MQTT status
updateMqttStatus();
@@ -388,6 +441,23 @@ async function initDashboard() {
startRefreshIntervals();
}
// Populate channel selector dropdown
function populateChannelSelector() {
const channelSelect = document.getElementById('message-channel');
if (!channelSelect) return;
// Clear existing options
channelSelect.innerHTML = '';
// Add options from config
Object.entries(state.channelConfig).forEach(([channelNum, channelName]) => {
const option = document.createElement('option');
option.value = channelNum;
option.textContent = channelName;
channelSelect.appendChild(option);
});
}
// Update MQTT Status
async function updateMqttStatus() {
try {
@@ -504,6 +574,7 @@ function displayMessages(messages, containerId) {
if (!existingIds.has(msgId)) {
// Create new message element
const channelColor = msg.channel !== null && msg.channel !== undefined ? ui.getChannelColor(msg.channel) : '#2563eb';
const messageHtml = `
<div class="message-item" data-message-id="${msgId}">
<div class="message-header">
@@ -513,13 +584,11 @@ function displayMessages(messages, containerId) {
<span class="message-time" data-timestamp="${msg.created_at}">${ui.formatRelativeTime(msg.created_at)}</span>
</div>
<div class="message-text">${escapeHtml(msg.text || '')}</div>
${msg.rx_snr || msg.rx_rssi ? `
<div class="message-meta">
${msg.rx_snr ? `SNR: ${msg.rx_snr.toFixed(1)} dB` : ''}
${msg.rx_rssi ? ` | RSSI: ${msg.rx_rssi} dBm` : ''}
${msg.channel !== null ? ` | Channel: ${msg.channel}` : ''}
</div>
` : ''}
<div class="message-meta">
${msg.channel !== null && msg.channel !== undefined ? `<span class="message-channel" style="background-color: ${channelColor};">${ui.getChannelName(msg.channel)}</span>` : ''}
${msg.rx_snr ? ` | SNR: ${msg.rx_snr.toFixed(1)} dB` : ''}
${msg.rx_rssi ? ` | RSSI: ${msg.rx_rssi} dBm` : ''}
</div>
</div>
`;
@@ -590,108 +659,15 @@ function displayNodes(nodes) {
return;
}
// Build a map of node IDs to nodes
const nodeMap = new Map();
nodes.forEach(node => {
nodeMap.set(node.node_id, node);
});
// Clear and rebuild to ensure proper sorting
container.innerHTML = '';
// Get existing node elements
const existingNodes = container.querySelectorAll('.node-card');
const existingIds = new Set();
existingNodes.forEach(elem => {
const nodeId = elem.getAttribute('data-node-id');
if (nodeId) existingIds.add(nodeId);
});
// Remove nodes that are no longer in the list
existingNodes.forEach(elem => {
const nodeId = elem.getAttribute('data-node-id');
if (nodeId && !nodeMap.has(nodeId)) {
elem.remove();
}
});
// Add new nodes or update existing ones
// Add nodes in the order they come from API (already sorted)
nodes.forEach(node => {
const lastHeard = node.last_heard ? new Date(node.last_heard) : null;
const isOnline = lastHeard && (Date.now() - lastHeard.getTime()) < 900000; // 15 minutes
if (existingIds.has(node.node_id)) {
// Update existing node card content
const card = container.querySelector(`[data-node-id="${node.node_id}"]`);
if (card) {
// Update node name
const nameElem = card.querySelector('.node-name');
if (nameElem) nameElem.textContent = node.long_name || node.short_name || 'Unknown';
// Update badge
const badge = card.querySelector('.node-badge');
if (badge) {
badge.className = `node-badge ${isOnline ? 'online' : 'offline'}`;
badge.textContent = isOnline ? 'Online' : 'Offline';
}
// Update last heard time
const timeElem = card.querySelector('.relative-time');
if (timeElem && node.last_heard) {
timeElem.setAttribute('data-timestamp', node.last_heard);
timeElem.textContent = ui.formatRelativeTime(node.last_heard);
}
// Update battery if present
const nodeInfo = card.querySelector('.node-info');
if (nodeInfo && node.battery_level) {
// Check if battery row already exists
let batteryRow = null;
nodeInfo.querySelectorAll('.node-info-row').forEach(row => {
const label = row.querySelector('span:first-child');
if (label && label.textContent.includes('Battery:')) {
batteryRow = row;
}
});
if (!batteryRow) {
nodeInfo.insertAdjacentHTML('beforeend',
`<div class="node-info-row"><span>Battery:</span><span>${node.battery_level}%</span></div>`
);
} else {
// Update existing battery value
const valueSpan = batteryRow.querySelector('span:last-child');
if (valueSpan) valueSpan.textContent = `${node.battery_level}%`;
}
}
// Update or add globe button if location is available
const headerDiv = card.querySelector('.node-card-header > div:last-child');
if (headerDiv && node.latitude && node.longitude) {
let globeButton = headerDiv.querySelector('a[href*="openstreetmap"]');
if (!globeButton) {
const globeHtml = `<a href="https://www.openstreetmap.org/?mlat=${node.latitude}&mlon=${node.longitude}&zoom=11"
target="_blank"
rel="noopener noreferrer"
onclick="event.stopPropagation()"
style="background: #4CAF50; color: white; padding: 4px 8px; border-radius: 4px; text-decoration: none; font-size: 14px; line-height: 1;">🌍</a>`;
const badge = headerDiv.querySelector('.node-badge');
if (badge) {
badge.insertAdjacentHTML('beforebegin', globeHtml);
}
} else {
// Update href if coordinates changed
globeButton.href = `https://www.openstreetmap.org/?mlat=${node.latitude}&mlon=${node.longitude}&zoom=11`;
}
} else if (headerDiv && !node.latitude) {
// Remove globe button if location is no longer available
const globeButton = headerDiv.querySelector('a[href*="openstreetmap"]');
if (globeButton) {
globeButton.remove();
}
}
}
} else {
// Create new node card
const nodeHtml = `
const nodeHtml = `
<div class="node-card" data-node-id="${escapeHtml(node.node_id)}">
<div class="node-card-header">
<div>
@@ -719,15 +695,14 @@ function displayNodes(nodes) {
</div>
`;
container.insertAdjacentHTML('beforeend', nodeHtml);
container.insertAdjacentHTML('beforeend', nodeHtml);
// Add click handler for the new card
const newCard = container.querySelector(`[data-node-id="${node.node_id}"]`);
if (newCard) {
newCard.addEventListener('click', () => {
window.showNodeDetail(node.node_id);
});
}
// Add click handler for the new card
const newCard = container.lastElementChild;
if (newCard) {
newCard.addEventListener('click', () => {
window.showNodeDetail(node.node_id);
});
}
});
}
@@ -1063,7 +1038,7 @@ document.getElementById('purge-form')?.addEventListener('submit', async (e) => {
try {
const result = await api.purgeData(hours);
alert(`Successfully purged:\n${result.deleted.messages} messages\n${result.deleted.positions} positions\n${result.deleted.telemetry} telemetry records`);
alert(`Successfully purged:\n${result.deleted.messages} messages\n${result.deleted.positions} positions\n${result.deleted.telemetry} telemetry records\n${result.deleted.nodes} nodes`);
// Reload stats
loadOverview();