fix: persist chat uploads on /data volume (broken images) + prevent duplicate chat sockets
- Broken images: UPLOADS_DIR/REC_DIR/TRANS_DIR were server/<dir> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+6
-3
@@ -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');
|
||||
|
||||
@@ -788,7 +788,7 @@
|
||||
<body>
|
||||
<script src="/icons.js?v=4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||
<script>window.__BUILD='2026-07-07-batch43';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-07-batch44';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
||||
// (emojis stay as plain Unicode). (#5)
|
||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||
@@ -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(_){}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user