Sort order and purging fix
This commit is contained in:
@@ -287,6 +287,13 @@ Log level can be configured with `LOG_LEVEL` in `.env` (debug, info, warn, error
|
|||||||
2. Check SESSION_SECRET is set in `.env`
|
2. Check SESSION_SECRET is set in `.env`
|
||||||
3. Clear browser cookies and try again
|
3. Clear browser cookies and try again
|
||||||
|
|
||||||
|
## T-Deck
|
||||||
|
|
||||||
|
- It's touch screen, which is often easier than using the trackball.
|
||||||
|
- To pair over Bluetooth, power on the T-Deck and LONG PRESS (about 2 seconds) the Meshtastic logo.
|
||||||
|
- To set the timezone properly, it should be `PST8PDT,M3.2.0,M11.1.0` for PST
|
||||||
|
- To get map tiles: https://www.jeffgeerling.com/blog/2025/adding-gps-and-grid-maps-my-meshtastic-t-deck
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|||||||
@@ -351,6 +351,16 @@ body {
|
|||||||
color: var(--text-secondary);
|
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 */
|
||||||
.nodes-grid {
|
.nodes-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
+1
-3
@@ -119,9 +119,7 @@
|
|||||||
<option value="474572292">!1c496604</option>
|
<option value="474572292">!1c496604</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="message-channel">
|
<select id="message-channel">
|
||||||
<option value="0">LongFast (public)</option>
|
<!-- Options populated dynamically from config -->
|
||||||
<option value="1">Aether (private)</option>
|
|
||||||
<option value="2">Other (2)</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn btn-primary">Send</button>
|
<button type="submit" class="btn btn-primary">Send</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+79
-104
@@ -18,7 +18,8 @@ const state = {
|
|||||||
reconnectAttempts: 0,
|
reconnectAttempts: 0,
|
||||||
maxReconnectAttempts: 5,
|
maxReconnectAttempts: 5,
|
||||||
reconnectDelay: 5000, // 5 seconds
|
reconnectDelay: 5000, // 5 seconds
|
||||||
isReconnecting: false
|
isReconnecting: false,
|
||||||
|
channelConfig: {} // Channel name configuration
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle authentication errors (401)
|
// Handle authentication errors (401)
|
||||||
@@ -189,6 +190,10 @@ const api = {
|
|||||||
|
|
||||||
async getMqttStatus() {
|
async getMqttStatus() {
|
||||||
return this.request('/api/mqtt/status');
|
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));
|
const bucketIndex = Math.min(7, Math.floor(minutesSince / bucketSize));
|
||||||
|
|
||||||
return colors[bucketIndex];
|
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
|
// Set user info
|
||||||
document.getElementById('user-info').textContent = state.user.username;
|
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
|
// Update MQTT status
|
||||||
updateMqttStatus();
|
updateMqttStatus();
|
||||||
|
|
||||||
@@ -388,6 +441,23 @@ async function initDashboard() {
|
|||||||
startRefreshIntervals();
|
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
|
// Update MQTT Status
|
||||||
async function updateMqttStatus() {
|
async function updateMqttStatus() {
|
||||||
try {
|
try {
|
||||||
@@ -504,6 +574,7 @@ function displayMessages(messages, containerId) {
|
|||||||
|
|
||||||
if (!existingIds.has(msgId)) {
|
if (!existingIds.has(msgId)) {
|
||||||
// Create new message element
|
// Create new message element
|
||||||
|
const channelColor = msg.channel !== null && msg.channel !== undefined ? ui.getChannelColor(msg.channel) : '#2563eb';
|
||||||
const messageHtml = `
|
const messageHtml = `
|
||||||
<div class="message-item" data-message-id="${msgId}">
|
<div class="message-item" data-message-id="${msgId}">
|
||||||
<div class="message-header">
|
<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>
|
<span class="message-time" data-timestamp="${msg.created_at}">${ui.formatRelativeTime(msg.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text">${escapeHtml(msg.text || '')}</div>
|
<div class="message-text">${escapeHtml(msg.text || '')}</div>
|
||||||
${msg.rx_snr || msg.rx_rssi ? `
|
|
||||||
<div class="message-meta">
|
<div class="message-meta">
|
||||||
${msg.rx_snr ? `SNR: ${msg.rx_snr.toFixed(1)} dB` : ''}
|
${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` : ''}
|
${msg.rx_rssi ? ` | RSSI: ${msg.rx_rssi} dBm` : ''}
|
||||||
${msg.channel !== null ? ` | Channel: ${msg.channel}` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -590,107 +659,14 @@ function displayNodes(nodes) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a map of node IDs to nodes
|
// Clear and rebuild to ensure proper sorting
|
||||||
const nodeMap = new Map();
|
container.innerHTML = '';
|
||||||
nodes.forEach(node => {
|
|
||||||
nodeMap.set(node.node_id, node);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get existing node elements
|
// Add nodes in the order they come from API (already sorted)
|
||||||
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
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const lastHeard = node.last_heard ? new Date(node.last_heard) : null;
|
const lastHeard = node.last_heard ? new Date(node.last_heard) : null;
|
||||||
const isOnline = lastHeard && (Date.now() - lastHeard.getTime()) < 900000; // 15 minutes
|
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" data-node-id="${escapeHtml(node.node_id)}">
|
||||||
<div class="node-card-header">
|
<div class="node-card-header">
|
||||||
@@ -722,13 +698,12 @@ function displayNodes(nodes) {
|
|||||||
container.insertAdjacentHTML('beforeend', nodeHtml);
|
container.insertAdjacentHTML('beforeend', nodeHtml);
|
||||||
|
|
||||||
// Add click handler for the new card
|
// Add click handler for the new card
|
||||||
const newCard = container.querySelector(`[data-node-id="${node.node_id}"]`);
|
const newCard = container.lastElementChild;
|
||||||
if (newCard) {
|
if (newCard) {
|
||||||
newCard.addEventListener('click', () => {
|
newCard.addEventListener('click', () => {
|
||||||
window.showNodeDetail(node.node_id);
|
window.showNodeDetail(node.node_id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,7 +1038,7 @@ document.getElementById('purge-form')?.addEventListener('submit', async (e) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.purgeData(hours);
|
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
|
// Reload stats
|
||||||
loadOverview();
|
loadOverview();
|
||||||
|
|||||||
@@ -50,5 +50,17 @@ module.exports = {
|
|||||||
// Logging configuration
|
// Logging configuration
|
||||||
logging: {
|
logging: {
|
||||||
level: process.env.LOG_LEVEL || 'info'
|
level: process.env.LOG_LEVEL || 'info'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Channel configuration
|
||||||
|
channels: {
|
||||||
|
0: process.env.CHANNEL_0_NAME || 'LongFast',
|
||||||
|
1: process.env.CHANNEL_1_NAME || 'Aether',
|
||||||
|
2: process.env.CHANNEL_2_NAME || 'Channel 2',
|
||||||
|
3: process.env.CHANNEL_3_NAME || 'Channel 3',
|
||||||
|
4: process.env.CHANNEL_4_NAME || 'Channel 4',
|
||||||
|
5: process.env.CHANNEL_5_NAME || 'Channel 5',
|
||||||
|
6: process.env.CHANNEL_6_NAME || 'Channel 6',
|
||||||
|
7: process.env.CHANNEL_7_NAME || 'Channel 7'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+68
-2
@@ -34,7 +34,7 @@ const nodeQueries = {
|
|||||||
hardware_model = COALESCE(excluded.hardware_model, hardware_model),
|
hardware_model = COALESCE(excluded.hardware_model, hardware_model),
|
||||||
role = COALESCE(excluded.role, role),
|
role = COALESCE(excluded.role, role),
|
||||||
firmware_version = COALESCE(excluded.firmware_version, firmware_version),
|
firmware_version = COALESCE(excluded.firmware_version, firmware_version),
|
||||||
last_heard = COALESCE(excluded.last_heard, CURRENT_TIMESTAMP),
|
last_heard = CURRENT_TIMESTAMP,
|
||||||
battery_level = COALESCE(excluded.battery_level, battery_level),
|
battery_level = COALESCE(excluded.battery_level, battery_level),
|
||||||
voltage = COALESCE(excluded.voltage, voltage),
|
voltage = COALESCE(excluded.voltage, voltage),
|
||||||
channel_utilization = COALESCE(excluded.channel_utilization, channel_utilization),
|
channel_utilization = COALESCE(excluded.channel_utilization, channel_utilization),
|
||||||
@@ -62,11 +62,77 @@ const nodeQueries = {
|
|||||||
(SELECT altitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as altitude,
|
(SELECT altitude FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as altitude,
|
||||||
(SELECT timestamp FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as position_timestamp
|
(SELECT timestamp FROM positions WHERE node_id = n.node_id ORDER BY timestamp DESC LIMIT 1) as position_timestamp
|
||||||
FROM nodes n
|
FROM nodes n
|
||||||
ORDER BY n.last_heard DESC
|
ORDER BY
|
||||||
|
COALESCE(datetime(n.last_heard), datetime('1970-01-01')) DESC
|
||||||
`),
|
`),
|
||||||
|
|
||||||
updateNodeLastHeard: db.prepare(`
|
updateNodeLastHeard: db.prepare(`
|
||||||
UPDATE nodes SET last_heard = CURRENT_TIMESTAMP WHERE node_id = ?
|
UPDATE nodes SET last_heard = CURRENT_TIMESTAMP WHERE node_id = ?
|
||||||
|
`),
|
||||||
|
|
||||||
|
updateNullLastHeard: db.prepare(`
|
||||||
|
UPDATE nodes
|
||||||
|
SET last_heard = (
|
||||||
|
SELECT MAX(latest_time)
|
||||||
|
FROM (
|
||||||
|
SELECT MAX(created_at) as latest_time FROM messages WHERE from_node = nodes.node_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT MAX(timestamp) as latest_time FROM positions WHERE node_id = nodes.node_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT MAX(timestamp) as latest_time FROM telemetry WHERE node_id = nodes.node_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE last_heard IS NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM messages WHERE from_node = nodes.node_id
|
||||||
|
UNION
|
||||||
|
SELECT 1 FROM positions WHERE node_id = nodes.node_id
|
||||||
|
UNION
|
||||||
|
SELECT 1 FROM telemetry WHERE node_id = nodes.node_id
|
||||||
|
)
|
||||||
|
`),
|
||||||
|
|
||||||
|
getOldNodesWithData: db.prepare(`
|
||||||
|
SELECT
|
||||||
|
n.node_id,
|
||||||
|
n.last_heard,
|
||||||
|
(SELECT COUNT(*) FROM messages WHERE from_node = n.node_id) as message_count,
|
||||||
|
(SELECT COUNT(*) FROM positions WHERE node_id = n.node_id) as position_count,
|
||||||
|
(SELECT COUNT(*) FROM telemetry WHERE node_id = n.node_id) as telemetry_count
|
||||||
|
FROM nodes n
|
||||||
|
WHERE datetime(last_heard) < datetime('now', '-' || ? || ' hours') OR last_heard IS NULL
|
||||||
|
`),
|
||||||
|
|
||||||
|
deleteOldNodes: db.prepare(`
|
||||||
|
DELETE FROM nodes
|
||||||
|
WHERE (
|
||||||
|
datetime(last_heard) < datetime('now', '-' || ? || ' hours')
|
||||||
|
OR (
|
||||||
|
last_heard IS NULL
|
||||||
|
AND node_id NOT IN (
|
||||||
|
SELECT DISTINCT from_node FROM messages WHERE from_node IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT node_id FROM positions WHERE node_id IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT node_id FROM telemetry WHERE node_id IS NOT NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND node_id NOT IN (
|
||||||
|
SELECT DISTINCT from_node FROM messages
|
||||||
|
WHERE from_node IS NOT NULL
|
||||||
|
AND datetime(created_at) >= datetime('now', '-' || ? || ' hours')
|
||||||
|
)
|
||||||
|
AND node_id NOT IN (
|
||||||
|
SELECT DISTINCT node_id FROM positions
|
||||||
|
WHERE node_id IS NOT NULL
|
||||||
|
AND datetime(timestamp) >= datetime('now', '-' || ? || ' hours')
|
||||||
|
)
|
||||||
|
AND node_id NOT IN (
|
||||||
|
SELECT DISTINCT node_id FROM telemetry
|
||||||
|
WHERE node_id IS NOT NULL
|
||||||
|
AND datetime(timestamp) >= datetime('now', '-' || ? || ' hours')
|
||||||
|
)
|
||||||
`)
|
`)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+32
-5
@@ -227,11 +227,31 @@ router.post('/purge', requireAuth, (req, res) => {
|
|||||||
const { hours } = req.body;
|
const { hours } = req.body;
|
||||||
const hoursToKeep = hours || 720; // Default to 30 days (720 hours)
|
const hoursToKeep = hours || 720; // Default to 30 days (720 hours)
|
||||||
|
|
||||||
const messagesDeleted = messageQueries.deleteOldMessages.run(hoursToKeep);
|
logger.info(`Starting purge of data older than ${hoursToKeep} hours`);
|
||||||
const positionsDeleted = positionQueries.deleteOldPositions.run(hoursToKeep);
|
|
||||||
const telemetryDeleted = telemetryQueries.deleteOldTelemetry.run(hoursToKeep);
|
|
||||||
|
|
||||||
logger.info(`Data purged: ${messagesDeleted.changes} messages, ${positionsDeleted.changes} positions, ${telemetryDeleted.changes} telemetry records`);
|
// First, update any nodes with NULL last_heard based on their most recent data
|
||||||
|
const nullLastHeardUpdated = nodeQueries.updateNullLastHeard.run();
|
||||||
|
logger.info(`Updated ${nullLastHeardUpdated.changes} nodes with NULL last_heard`);
|
||||||
|
|
||||||
|
const messagesDeleted = messageQueries.deleteOldMessages.run(hoursToKeep);
|
||||||
|
logger.info(`Deleted ${messagesDeleted.changes} old messages`);
|
||||||
|
|
||||||
|
const positionsDeleted = positionQueries.deleteOldPositions.run(hoursToKeep);
|
||||||
|
logger.info(`Deleted ${positionsDeleted.changes} old positions`);
|
||||||
|
|
||||||
|
const telemetryDeleted = telemetryQueries.deleteOldTelemetry.run(hoursToKeep);
|
||||||
|
logger.info(`Deleted ${telemetryDeleted.changes} old telemetry records`);
|
||||||
|
|
||||||
|
// Check which old nodes still have data before deleting
|
||||||
|
const oldNodesWithData = nodeQueries.getOldNodesWithData.all(hoursToKeep);
|
||||||
|
oldNodesWithData.forEach(node => {
|
||||||
|
logger.info(`Old node ${node.node_id} (last_heard: ${node.last_heard}): ${node.message_count} messages, ${node.position_count} positions, ${node.telemetry_count} telemetry`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodesDeleted = nodeQueries.deleteOldNodes.run(hoursToKeep, hoursToKeep, hoursToKeep, hoursToKeep);
|
||||||
|
logger.info(`Deleted ${nodesDeleted.changes} old nodes`);
|
||||||
|
|
||||||
|
logger.info(`Data purged: ${messagesDeleted.changes} messages, ${positionsDeleted.changes} positions, ${telemetryDeleted.changes} telemetry records, ${nodesDeleted.changes} nodes`);
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
const timePeriod = hoursToKeep < 24
|
const timePeriod = hoursToKeep < 24
|
||||||
@@ -250,7 +270,8 @@ router.post('/purge', requireAuth, (req, res) => {
|
|||||||
deleted: {
|
deleted: {
|
||||||
messages: messagesDeleted.changes,
|
messages: messagesDeleted.changes,
|
||||||
positions: positionsDeleted.changes,
|
positions: positionsDeleted.changes,
|
||||||
telemetry: telemetryDeleted.changes
|
telemetry: telemetryDeleted.changes,
|
||||||
|
nodes: nodesDeleted.changes
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -266,4 +287,10 @@ router.get('/mqtt/status', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get channel configuration
|
||||||
|
router.get('/config/channels', requireAuth, (req, res) => {
|
||||||
|
const config = require('../config/config');
|
||||||
|
res.json(config.channels);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user