From 1128f9811a515537a59ee6d484db7fd83fece83d Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 7 Jul 2026 13:09:24 +0530 Subject: [PATCH] fix: persist chat uploads on /data volume (broken images) + prevent duplicate chat sockets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Broken images: UPLOADS_DIR/REC_DIR/TRANS_DIR were server/ INSIDE the image, so every deploy.sh rebuild wiped uploaded files — old images 404'd ('broken image') though their DB rows survived. Make them env-overridable and point prod at /data/uploads|recordings|transcripts (persistent volume), matching DB/downloads. NOTE: files already lost to prior rebuilds can't be recovered; new uploads persist. - Duplicate notifications: harden connectChatWs — close/detach any prior socket before opening a new one and keep a single pending reconnect timer, so a flaky reconnect can't leave two live sockets delivering every event/notification twice. Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 5 +++++ server/config.js | 9 ++++++--- server/public/home.html | 10 ++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0a8b6f1..e720c3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,11 @@ services: # Desktop installers + auto-update feed live on the persistent volume so uploaded # builds survive image rebuilds (a plain image path would be wiped on every deploy). - DOWNLOADS_DIR=/data/downloads + # Chat uploads / recordings / transcripts on the persistent volume too, so they survive image + # rebuilds (otherwise old shared images 404 as "broken image" after every deploy). + - UPLOADS_DIR=/data/uploads + - REC_DIR=/data/recordings + - TRANS_DIR=/data/transcripts # Secrets (TURN credentials, SSO_SECRET, BIZGAZE_WEBHOOK_URL, etc.) live in # a .env file next to this compose file. It is gitignored — never committed. # See .env.example for the expected keys. diff --git a/server/config.js b/server/config.js index 3637010..8025e21 100644 --- a/server/config.js +++ b/server/config.js @@ -3,9 +3,12 @@ const fs = require('fs'); const path = require('path'); const PUBLIC_DIR = path.join(__dirname, 'public'); -const REC_DIR = path.join(__dirname, 'recordings'); -const TRANS_DIR = path.join(__dirname, 'transcripts'); -const UPLOADS_DIR = path.join(__dirname, 'uploads'); +// Uploaded chat files, recordings and transcripts MUST live on the persistent volume (like the DB +// and downloads). With the old in-image path, every deploy.sh rebuild wiped them — old images/files +// then 404 ("broken image") while their DB rows survive. Overridable so prod points them at /data. +const REC_DIR = process.env.REC_DIR || path.join(__dirname, 'recordings'); +const TRANS_DIR = process.env.TRANS_DIR || path.join(__dirname, 'transcripts'); +const UPLOADS_DIR = process.env.UPLOADS_DIR || path.join(__dirname, 'uploads'); // Desktop installers + auto-update feed (latest.yml). Override with DOWNLOADS_DIR to point at a // mounted volume in production; IT drops the electron-builder dist/ output here. const DOWNLOADS_DIR = process.env.DOWNLOADS_DIR || path.join(__dirname, 'downloads'); diff --git a/server/public/home.html b/server/public/home.html index f36eaee..5210cdd 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -788,7 +788,7 @@ - @@ -2252,12 +2252,18 @@ async function refreshOpenThread(){ }); if(changed){ THREAD.sort((a,b)=>(a.created_at||0)-(b.created_at||0)); THREAD_CACHE.set(kind+':'+id, THREAD.slice()); renderThread(); } } +let _chatReconnectT=null; function connectChatWs(){ try{ + // Defensive: drop any prior socket first so a flaky reconnect can't leave TWO live sockets — + // which would deliver every event (and notification) twice. Detach its handlers before closing + // so its onclose doesn't schedule yet another reconnect. + if(chatWs){ try{ chatWs.onopen=chatWs.onmessage=chatWs.onclose=chatWs.onerror=null; chatWs.close(); }catch(_){} chatWs=null; } + if(_chatReconnectT){ clearTimeout(_chatReconnectT); _chatReconnectT=null; } chatWs=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws'); chatWs.onopen=()=>{ try{ chatWs.send(JSON.stringify({type:'chat-hello'})); }catch(_){} if(_chatConnectedOnce) resyncChat(); _chatConnectedOnce=true; }; chatWs.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch(_){ return; } if(d.type==='chat-message' && d.message) onChatMessage(d.message); else if(d.type==='chat-deleted') onChatDeleted(d); else if(d.type==='chat-edited') onChatEdited(d); else if(d.type==='chat-reaction') onChatReaction(d); else if(d.type==='poll-update' && d.poll) onPollUpdate(d); else if(d.type==='chat-read') onChatRead(d); else if(d.type==='chat-delivered') onChatDelivered(d); else if(d.type==='group-read') onGroupRead(d); else if(d.type==='group-call') onGroupCall(d); else if(d.type==='dm-call') onDmCall(d); else if(d.type==='presence') onPresence(d); else if(d.type==='chat-typing') onTyping(d); else if(d.type==='group-update') onGroupUpdate(d); else if(d.type==='call-invite') showCallInvite(d.room, d.byName); else if(d.type==='meeting-invite') showMeetingInvite(d.meeting); else if(d.type==='meeting-reminder') showMeetingReminder(d.meeting); else if(d.type==='meeting-cancelled') showMeetingCancelled(d.meeting); else if(d.type==='group-role') onGroupRole(d); }; - chatWs.onclose=()=>{ setTimeout(connectChatWs, 3000); }; // auto-reconnect + chatWs.onclose=()=>{ if(_chatReconnectT) clearTimeout(_chatReconnectT); _chatReconnectT=setTimeout(connectChatWs, 3000); }; // auto-reconnect (single pending timer) }catch(_){} }