diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 0310ca9..8b5eb78 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -132,6 +132,7 @@ export const WorkspaceView: React.FC = ({ // Dedicated WebSocket connection & status for live indicator const wsRef = useRef(null); const wsReconnectTimerRef = useRef(null); + const wsReconnectDelayRef = useRef(2000); const [wsConnected, setWsConnected] = useState(false); // Folder state (synchronized with server) @@ -970,6 +971,7 @@ export const WorkspaceView: React.FC = ({ ws.onopen = () => { if (!isMounted) return; + wsReconnectDelayRef.current = 2000; setWsConnected(true); api.getFolders().then(setCustomFolders).catch(() => {}); api.getSharedFolders().then(setSharedFolders).catch(() => {}); @@ -979,7 +981,9 @@ export const WorkspaceView: React.FC = ({ if (!isMounted) return; setWsConnected(false); clearTimeout(wsReconnectTimerRef.current); - wsReconnectTimerRef.current = setTimeout(connect, 2000); + const delay = wsReconnectDelayRef.current; + wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5)); + wsReconnectTimerRef.current = setTimeout(connect, delay); }; ws.onerror = () => { @@ -997,7 +1001,9 @@ export const WorkspaceView: React.FC = ({ if (!isMounted) return; setWsConnected(false); clearTimeout(wsReconnectTimerRef.current); - wsReconnectTimerRef.current = setTimeout(connect, 3000); + const delay = wsReconnectDelayRef.current; + wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5)); + wsReconnectTimerRef.current = setTimeout(connect, delay); } }; @@ -1024,6 +1030,44 @@ export const WorkspaceView: React.FC = ({ }; }, [onStatsChange]); + // Fallback HTTP polling when WebSocket is disconnected (e.g. corporate proxy / firewall blocking WebSockets) + useEffect(() => { + if (wsConnected) return; + + let isMounted = true; + const pollInterval = setInterval(async () => { + if (!isMounted || document.visibilityState !== 'visible') return; + + // 1. Poll lock status of currently selected document + if (selectedDocIdRef.current) { + try { + const res = await api.getDocLock(selectedDocIdRef.current); + if (res.locked && res.lock && res.lock.userId !== currentUserIdRef.current) { + setActiveLock(res.lock); + } else { + setActiveLock(null); + } + } catch {} + } + + // 2. Poll document content if not actively typing and document has changed on server + if (selectedDocIdRef.current && !isEditorFocusedRef.current && !pendingSaveDocRef.current) { + try { + const fresh = await api.getDocument(selectedDocIdRef.current); + if (fresh && (!selectedDoc || fresh.updatedAt !== selectedDoc.updatedAt)) { + setSelectedDoc(fresh); + setDocs((prev) => prev.map((d) => (d.id === fresh.id ? fresh : d))); + } + } catch {} + } + }, 4000); + + return () => { + isMounted = false; + clearInterval(pollInterval); + }; + }, [wsConnected, selectedDoc?.updatedAt]); + const openShareModal = async () => { if (!selectedDoc) return; setShowShareModal(true); @@ -4001,11 +4045,15 @@ export const WorkspaceView: React.FC = ({ className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${ wsConnected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500' - : 'bg-canvas border-border text-muted' + : 'bg-amber-500/10 border-amber-500/30 text-amber-500' }`} - title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'} + title={ + wsConnected + ? 'Live synchronisiert (WebSocket online)' + : 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv' + } > - + @@ -4037,11 +4085,15 @@ export const WorkspaceView: React.FC = ({ className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${ wsConnected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500' - : 'bg-canvas border-border text-muted' + : 'bg-amber-500/10 border-amber-500/30 text-amber-500' }`} - title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'} + title={ + wsConnected + ? 'Live synchronisiert (WebSocket online)' + : 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv' + } > - + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1288836..937e1c9 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1048,6 +1048,7 @@ let wsListeners: ((msg: WSMessage) => void)[] = []; let wsStatusListeners: ((connected: boolean) => void)[] = []; let isWSConnected = false; let manualDisconnect = false; +let wsReconnectDelay = 2000; export function buildWebSocketURL(listId?: string): string { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -1097,6 +1098,7 @@ export function connectWebSocket(): void { activeGlobalWS = ws; ws.onopen = () => { + wsReconnectDelay = 2000; notifyStatusChange(true); }; @@ -1121,9 +1123,11 @@ export function connectWebSocket(): void { notifyStatusChange(false); if (!manualDisconnect && getToken()) { clearTimeout(wsReconnectTimer); + const delay = wsReconnectDelay; + wsReconnectDelay = Math.min(30000, Math.round(wsReconnectDelay * 1.5)); wsReconnectTimer = setTimeout(() => { connectWebSocket(); - }, 3000); + }, delay); } };