@@ -102,6 +102,51 @@ _setupSocketListeners() {
102102 this . socket . emit ( 'visibility-change' , { visible : ! document . hidden } ) ;
103103 this . socket . emit ( 'get-channels' ) ;
104104 this . socket . emit ( 'get-server-settings' ) ;
105+
106+ // (#5391) Watchdog: if the socket connects but channels-list never
107+ // arrives, the session is in a broken state that the auth middleware
108+ // didn't catch (DB hiccup mid-handshake, getEnrichedChannels throwing,
109+ // user row out of sync, etc.). The visible symptom is the sidebar
110+ // sitting empty forever and the user not knowing whether to refresh.
111+ // After 10 s of silence, fall back to a deterministic HTTP token check
112+ // and either retry once or kick to /login. Cleared as soon as
113+ // channels-list lands (see the channels-list handler below).
114+ if ( this . _channelsWatchdog ) clearTimeout ( this . _channelsWatchdog ) ;
115+ this . _channelsListGotResponse = false ;
116+ this . _channelsWatchdog = setTimeout ( async ( ) => {
117+ if ( this . _channelsListGotResponse ) return ;
118+ try {
119+ const resp = await fetch ( '/api/auth/validate' , {
120+ headers : { 'Authorization' : 'Bearer ' + ( localStorage . getItem ( 'haven_token' ) || '' ) }
121+ } ) ;
122+ if ( resp . status === 401 || resp . status === 404 ) {
123+ // Token is stale or user row gone — same outcome as a socket
124+ // 'Invalid token' / 'Session expired'. Kick to login.
125+ localStorage . removeItem ( 'haven_token' ) ;
126+ localStorage . removeItem ( 'haven_user' ) ;
127+ localStorage . removeItem ( 'haven_sync_key' ) ;
128+ window . location . href = '/' ;
129+ return ;
130+ }
131+ } catch {
132+ // Network failure — leave it alone, the user can refresh manually.
133+ return ;
134+ }
135+ // Token is valid but channels never came. Retry once before giving up.
136+ if ( ! this . _channelsListGotResponse && this . socket ?. connected ) {
137+ console . warn ( '[#5391] channels-list missing after 10s, retrying get-channels' ) ;
138+ this . socket . emit ( 'get-channels' ) ;
139+ setTimeout ( ( ) => {
140+ if ( ! this . _channelsListGotResponse ) {
141+ // Server clearly can't fulfil get-channels for this session —
142+ // a full reload picks up any server-side fix and re-runs the
143+ // auth handshake from scratch.
144+ console . warn ( '[#5391] channels-list still missing after retry, forcing reload' ) ;
145+ window . location . reload ( ) ;
146+ }
147+ } , 5000 ) ;
148+ }
149+ } , 10000 ) ;
105150 if ( this . currentChannel ) {
106151 this . socket . emit ( 'enter-channel' , { code : this . currentChannel } ) ;
107152 // Reset pagination — reconnect replaces message list
@@ -417,6 +462,12 @@ _setupSocketListeners() {
417462 } ) ;
418463
419464 this . socket . on ( 'channels-list' , ( channels ) => {
465+ // (#5391) Cancel the channels-not-arriving watchdog
466+ this . _channelsListGotResponse = true ;
467+ if ( this . _channelsWatchdog ) {
468+ clearTimeout ( this . _channelsWatchdog ) ;
469+ this . _channelsWatchdog = null ;
470+ }
420471 // Detect if currentChannel's code was rotated while disconnected (stale code).
421472 // Capture the channel's ID from the old list before overwriting it.
422473 let rotatedChannelId = null ;
0 commit comments