fix: prevent page hang when device channel queries block

/api/messages and /api/messages/updates called get_channels_cached()
which blocks on device communication when cache is cold (up to 240s).
Now uses DB-cached channels for pkt_payload computation instead.

Frontend loadMessages() now has a 15s timeout with auto-retry
and clears the loading spinner on error instead of leaving it
spinning indefinitely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-23 21:26:26 +01:00
parent 343b6f40a8
commit dfc3b1403a
2 changed files with 50 additions and 24 deletions
+23 -22
View File
@@ -371,14 +371,15 @@ def get_messages():
) )
# Build channel secret lookup for pkt_payload computation # Build channel secret lookup for pkt_payload computation
# Use DB channels (fast) instead of get_channels_cached() which
# can block on device communication when cache is cold
channel_secrets = {} channel_secrets = {}
_, channels_list = get_channels_cached() db_channels = db.get_channels() if db else []
if channels_list: for ch_info in db_channels:
for ch_info in channels_list: ch_key = ch_info.get('secret', ch_info.get('key', ''))
ch_key = ch_info.get('key', '') ch_idx = ch_info.get('idx', ch_info.get('index'))
ch_idx = ch_info.get('index') if ch_key and ch_idx is not None:
if ch_key and ch_idx is not None: channel_secrets[ch_idx] = ch_key
channel_secrets[ch_idx] = ch_key
# Convert DB rows to frontend-compatible format # Convert DB rows to frontend-compatible format
messages = [] messages = []
@@ -485,15 +486,15 @@ def get_message_meta(msg_id):
txt_type = row.get('txt_type', 0) txt_type = row.get('txt_type', 0)
# Compute pkt_payload if not stored # Compute pkt_payload if not stored
# Use DB channels (fast) to avoid blocking on device communication
if not pkt_payload and sender_ts: if not pkt_payload and sender_ts:
_, channels_list = get_channels_cached() db_channels = db.get_channels() if db else []
channel_secrets = {} channel_secrets = {}
if channels_list: for ch_info in db_channels:
for ch_info in channels_list: ch_key = ch_info.get('secret', ch_info.get('key', ''))
ch_key = ch_info.get('key', '') ci = ch_info.get('idx', ch_info.get('index'))
ci = ch_info.get('index') if ch_key and ci is not None:
if ch_key and ci is not None: channel_secrets[ci] = ch_key
channel_secrets[ci] = ch_key
if ch_idx in channel_secrets: if ch_idx in channel_secrets:
raw_text = None raw_text = None
@@ -1698,17 +1699,17 @@ def get_messages_updates():
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
last_seen = {} last_seen = {}
# Get list of channels (cached) # Get list of channels from DB (fast) to avoid blocking on device
success_ch, channels = get_channels_cached() db = _get_db()
if not success_ch: db_channels = db.get_channels() if db else []
return jsonify({ # Normalize DB channel format to match expected structure
'success': False, channels = [{'index': ch['idx'], 'name': ch['name']} for ch in db_channels]
'error': 'Failed to get channels' if not channels:
}), 500 # Fallback: at least include Public channel
channels = [{'index': 0, 'name': 'Public'}]
# OPTIMIZATION: Read ALL messages ONCE (no channel filter) # OPTIMIZATION: Read ALL messages ONCE (no channel filter)
# Then compute per-channel statistics in memory # Then compute per-channel statistics in memory
db = _get_db()
if db: if db:
all_messages = db.get_channel_messages(limit=None, days=7) all_messages = db.get_channel_messages(limit=None, days=7)
else: else:
+27 -2
View File
@@ -854,7 +854,12 @@ async function loadMessages() {
url += '&days=7'; url += '&days=7';
} }
const response = await fetch(url); // Add timeout to prevent hanging spinner
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
@@ -863,11 +868,31 @@ async function loadMessages() {
updateLastRefresh(); updateLastRefresh();
} else { } else {
showNotification('Error loading messages: ' + data.error, 'danger'); showNotification('Error loading messages: ' + data.error, 'danger');
clearLoadingSpinner();
} }
} catch (error) { } catch (error) {
console.error('Error loading messages:', error); console.error('Error loading messages:', error);
updateStatus('disconnected'); updateStatus('disconnected');
showNotification('Failed to load messages', 'danger'); clearLoadingSpinner();
if (error.name === 'AbortError') {
showNotification('Loading messages timed out — retrying...', 'warning');
setTimeout(loadMessages, 2000);
} else {
showNotification('Failed to load messages', 'danger');
}
}
}
function clearLoadingSpinner() {
const container = document.getElementById('messagesList');
if (container && container.querySelector('.spinner-border')) {
container.innerHTML = `
<div class="empty-state">
<i class="bi bi-exclamation-triangle"></i>
<p>Could not load messages</p>
<small>Will retry automatically</small>
</div>
`;
} }
} }