fix: Resolve channel dropdown disappearing issue

- Add timeout protection (10s for channels, 15s for updates)
- Implement fallback mechanism to ensure Public channel always exists
- Fix race condition by loading channels before setupAutoRefresh()
- Add proper validation for API responses and channel data
- Improve error handling with detailed logging
- Prevent checkForUpdates() from running before channels are loaded

This fixes the recurring issue where the channel dropdown would
randomly become empty, especially visible on mobile devices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-23 21:44:34 +01:00
parent c8a2201c8c
commit a494f6b47d
+96 -28
View File
@@ -13,38 +13,35 @@ let lastSeenTimestamps = {}; // Track last seen message timestamp per channel
let unreadCounts = {}; // Track unread message counts per channel
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', async function() {
console.log('mc-webui initialized');
// Load last seen timestamps from localStorage
loadLastSeenTimestamps();
// Load channels list
loadChannels();
// Restore last selected channel from localStorage
const savedChannel = localStorage.getItem('mc_active_channel');
if (savedChannel !== null) {
currentChannelIdx = parseInt(savedChannel);
}
// Load archive list
loadArchiveList();
// Load initial messages
loadMessages();
// Setup auto-refresh
setupAutoRefresh();
// Setup event listeners
// Setup event listeners (do this early)
setupEventListeners();
// Setup emoji picker
setupEmojiPicker();
// Load device status
// CRITICAL: Load channels FIRST before anything else
// This ensures channels are available for checkForUpdates()
await loadChannels();
// Now load other data (can run in parallel)
loadArchiveList();
loadMessages();
loadStatus();
// Setup auto-refresh AFTER channels are loaded
setupAutoRefresh();
});
/**
@@ -647,13 +644,33 @@ function markChannelAsRead(channelIdx, timestamp) {
* Check for new messages across all channels
*/
async function checkForUpdates() {
// Don't check if channels aren't loaded yet
if (!availableChannels || availableChannels.length === 0) {
console.log('[checkForUpdates] Skipping - channels not loaded yet');
return;
}
try {
// Build query with last seen timestamps
const lastSeenParam = encodeURIComponent(JSON.stringify(lastSeenTimestamps));
const response = await fetch(`/api/messages/updates?last_seen=${lastSeenParam}`);
// Add timeout to prevent hanging
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s timeout
const response = await fetch(`/api/messages/updates?last_seen=${lastSeenParam}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`[checkForUpdates] HTTP ${response.status}: ${response.statusText}`);
return;
}
const data = await response.json();
if (data.success) {
if (data.success && data.channels) {
// Update unread counts
data.channels.forEach(channel => {
unreadCounts[channel.index] = channel.unread_count;
@@ -670,7 +687,11 @@ async function checkForUpdates() {
}
}
} catch (error) {
console.error('Error checking for updates:', error);
if (error.name === 'AbortError') {
console.warn('[checkForUpdates] Request timeout after 15s');
} else {
console.error('[checkForUpdates] Error:', error.message || error);
}
}
}
@@ -796,12 +817,24 @@ function setupEmojiPicker() {
async function loadChannels() {
try {
console.log('[loadChannels] Fetching channels from API...');
const response = await fetch('/api/channels');
const data = await response.json();
// Add timeout to prevent hanging
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
const response = await fetch('/api/channels', {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[loadChannels] API response:', data);
if (data.success) {
if (data.success && data.channels && data.channels.length > 0) {
availableChannels = data.channels;
console.log('[loadChannels] Channels loaded:', availableChannels.length);
populateChannelSelector(data.channels);
@@ -809,10 +842,30 @@ async function loadChannels() {
// Check for unread messages after channels are loaded
await checkForUpdates();
} else {
console.error('[loadChannels] Error loading channels:', data.error);
console.error('[loadChannels] Error loading channels:', data.error || 'No channels returned');
// Fallback: ensure at least Public channel exists
ensurePublicChannel();
}
} catch (error) {
console.error('[loadChannels] Exception:', error);
if (error.name === 'AbortError') {
console.error('[loadChannels] Request timeout after 10s');
} else {
console.error('[loadChannels] Exception:', error.message || error);
}
// Fallback: ensure at least Public channel exists
ensurePublicChannel();
}
}
/**
* Fallback: ensure Public channel exists in dropdown even if API fails
*/
function ensurePublicChannel() {
const selector = document.getElementById('channelSelector');
if (!selector || selector.options.length === 0) {
console.log('[ensurePublicChannel] Adding fallback Public channel');
availableChannels = [{index: 0, name: 'Public', key: ''}];
populateChannelSelector(availableChannels);
}
}
@@ -821,6 +874,16 @@ async function loadChannels() {
*/
function populateChannelSelector(channels) {
const selector = document.getElementById('channelSelector');
if (!selector) {
console.error('[populateChannelSelector] Channel selector element not found');
return;
}
// Validate input
if (!channels || !Array.isArray(channels) || channels.length === 0) {
console.warn('[populateChannelSelector] Invalid channels array, using fallback');
channels = [{index: 0, name: 'Public', key: ''}];
}
// Remove all options - we'll rebuild everything from API data
while (selector.options.length > 0) {
@@ -829,10 +892,14 @@ function populateChannelSelector(channels) {
// Add all channels from API (including Public at index 0)
channels.forEach(channel => {
const option = document.createElement('option');
option.value = channel.index;
option.textContent = channel.name;
selector.appendChild(option);
if (channel && typeof channel.index !== 'undefined' && channel.name) {
const option = document.createElement('option');
option.value = channel.index;
option.textContent = channel.name;
selector.appendChild(option);
} else {
console.warn('[populateChannelSelector] Skipping invalid channel:', channel);
}
});
// Restore selection (use currentChannelIdx from global state)
@@ -840,12 +907,13 @@ function populateChannelSelector(channels) {
// If the saved channel doesn't exist, fall back to Public (0)
if (selector.value !== currentChannelIdx.toString()) {
console.log(`[populateChannelSelector] Channel ${currentChannelIdx} not found, falling back to Public`);
currentChannelIdx = 0;
selector.value = 0;
localStorage.setItem('mc_active_channel', '0');
}
console.log(`Loaded ${channels.length} channels, active: ${currentChannelIdx}`);
console.log(`[populateChannelSelector] Loaded ${channels.length} channels, active: ${currentChannelIdx}`);
}
/**