Sort order and purging fix
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user