refactor(collab): remove live document collaboration and enforce strictly read-only sharing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s

This commit is contained in:
sandrodach committed 2026-10-27 17:08:38 +01:00
1 parent aea05e868a
commit 04fa774d10
16 files changed
+179 -3556

No files matched your search

+50 -201
View File
@@ -48,10 +48,9 @@ import {
getRealtimeTransport,
RealtimeTransport,
} from '../../lib/api';
import { DocSyncStatus } from '../../lib/useDocumentSync';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { useMobileActions } from '../../context/MobileActionContext';
import { DocumentEditor, DocSaveStatus, DocLiveState } from './DocumentEditor';
import { DocumentEditor, DocSaveStatus } from './DocumentEditor';
interface WorkspaceViewProps {
currentUser?: User | null;
@@ -328,13 +327,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
// Realtime connection status for top header indicator (identical to SharedListsView)
const [realtimeConnected, setRealtimeConnected] = useState<boolean>(() => isRealtimeConnected());
const [realtimeTransport, setRealtimeTransport] = useState<RealtimeTransport>(() => getRealtimeTransport());
const [activeDocSyncStatus, setActiveDocSyncStatus] = useState<DocSyncStatus>('connected');
const [docSaveStatus, setDocSaveStatus] = useState<DocSaveStatus>('saved');
const [docSaveError, setDocSaveError] = useState<string | null>(null);
const [docLiveState, setDocLiveState] = useState<DocLiveState>('idle');
const [docLiveInitiator, setDocLiveInitiator] = useState<string | null>(null);
const [showWsBlockedModal, setShowWsBlockedModal] = useState<boolean>(false);
const liveToggleRef = useRef<(() => void) | null>(null);
useEffect(() => {
const unsub = onRealtimeStatus((connected, transport) => {
@@ -1168,136 +1162,52 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Right Header Section: Save Status & Live Collaboration Button */}
<div className="flex items-center gap-2">
{subView === 'editor' && selectedDocId ? (
<>
{/* Save Status Pill */}
<div
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium border transition-colors ${
docSaveStatus === 'saved'
? 'bg-canvas border-border text-muted'
: docSaveStatus === 'saving'
? 'bg-panel border-accent/40 text-accent animate-pulse'
: docSaveStatus === 'unsaved'
? 'bg-status-warn/10 border-status-warn/30 text-status-warn'
: 'bg-status-err/10 border-status-err/30 text-status-err'
}`}
title={
docSaveStatus === 'saved'
? 'Alle Änderungen per HTTPS gesichert'
: docSaveStatus === 'saving'
? 'Änderungen werden per HTTPS gespeichert…'
: docSaveStatus === 'unsaved'
? 'Ungespeicherte Änderungen (Auto-Save aktiv)'
: docSaveError || 'Fehler beim Speichern'
}
>
{docSaveStatus === 'saved' && (
<>
<Check className="w-3.5 h-3.5 text-status-ok" />
<span>Gespeichert</span>
</>
)}
{docSaveStatus === 'saving' && (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin text-accent" />
<span>Speichert…</span>
</>
)}
{docSaveStatus === 'unsaved' && (
<>
<Clock className="w-3.5 h-3.5 text-status-warn" />
<span>Ungespeichert</span>
</>
)}
{docSaveStatus === 'error' && (
<>
<AlertTriangle className="w-3.5 h-3.5 text-status-err" />
<span>Fehler</span>
</>
)}
</div>
{/* Interactive Live Collaboration Button (replaces passive radio icon) */}
{docLiveState === 'locked_by_other' ? (
<div className="inline-flex items-center gap-1.5">
<div
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-warn/15 border border-status-warn/40 text-status-warn select-none"
title={`Live-Zusammenarbeit aktiv durch ${docLiveInitiator || 'anderer Nutzer'}`}
>
<Lock className="w-3.5 h-3.5 shrink-0" />
<span className="truncate max-w-[120px]">Live: {docLiveInitiator || 'Gesperrt'}</span>
</div>
{(selectedDocObj?.isOwner || selectedDocObj?.permission === 'owner') && (
<button
type="button"
onClick={() => liveToggleRef.current?.()}
className="px-2 py-1 rounded-btn text-xs font-semibold bg-status-warn/20 border border-status-warn/40 text-status-warn hover:bg-status-warn/30 transition-colors cursor-pointer"
title="Als Notiz-Besitzer Live-Sitzung beenden und Dokument freigeben"
>
Freigeben
</button>
)}
</div>
) : docLiveState === 'blocked' ? (
<div className="inline-flex items-center gap-1.5">
<button
type="button"
onClick={() => setShowWsBlockedModal(true)}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 transition-colors cursor-pointer"
title="WebSockets im Firmennetzwerk blockiert – Hier klicken für Infos"
>
<AlertTriangle className="w-3.5 h-3.5 text-status-err animate-pulse shrink-0" />
<span>Live blockiert</span>
</button>
<button
type="button"
onClick={() => liveToggleRef.current?.()}
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
title="Live-Sitzung beenden"
>
Beenden
</button>
</div>
) : docLiveState === 'connecting' ? (
<div className="inline-flex items-center gap-1.5">
<button
type="button"
disabled
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-amber-500/15 border border-amber-500/40 text-amber-500 opacity-80 cursor-wait"
>
<Loader2 className="w-3.5 h-3.5 animate-spin text-amber-500 shrink-0" />
<span>Verbinde…</span>
</button>
<button
type="button"
onClick={() => liveToggleRef.current?.()}
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
title="Verbindungsaufbau abbrechen"
>
Abbrechen
</button>
</div>
) : docLiveState === 'active' ? (
<button
type="button"
onClick={() => liveToggleRef.current?.()}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-semibold bg-emerald-500/15 border border-emerald-500/40 text-emerald-500 hover:bg-emerald-500/25 transition-colors cursor-pointer"
title="Live-Zusammenarbeit beenden (Tastendruck / WebSocket)"
>
<Radio className="w-3.5 h-3.5 text-emerald-500 animate-pulse shrink-0" />
<span>Live aktiv</span>
</button>
) : (
<button
type="button"
onClick={() => liveToggleRef.current?.()}
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-main hover:border-accent transition-colors cursor-pointer"
title="Live-Zusammenarbeit aktivieren (WebSockets)"
>
<Radio className="w-3.5 h-3.5 text-muted hover:text-accent shrink-0" />
<span>Live</span>
</button>
/* Save Status Pill */
<div
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium border transition-colors ${
docSaveStatus === 'saved'
? 'bg-canvas border-border text-muted'
: docSaveStatus === 'saving'
? 'bg-panel border-accent/40 text-accent animate-pulse'
: docSaveStatus === 'unsaved'
? 'bg-status-warn/10 border-status-warn/30 text-status-warn'
: 'bg-status-err/10 border-status-err/30 text-status-err'
}`}
title={
docSaveStatus === 'saved'
? 'Alle Änderungen per HTTPS gesichert'
: docSaveStatus === 'saving'
? 'Änderungen werden per HTTPS gespeichert…'
: docSaveStatus === 'unsaved'
? 'Ungespeicherte Änderungen (Auto-Save aktiv)'
: docSaveError || 'Fehler beim Speichern'
}
>
{docSaveStatus === 'saved' && (
<>
<Check className="w-3.5 h-3.5 text-status-ok" />
<span>Gespeichert</span>
</>
)}
</>
{docSaveStatus === 'saving' && (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin text-accent" />
<span>Speichert…</span>
</>
)}
{docSaveStatus === 'unsaved' && (
<>
<Clock className="w-3.5 h-3.5 text-status-warn" />
<span>Ungespeichert</span>
</>
)}
{docSaveStatus === 'error' && (
<>
<AlertTriangle className="w-3.5 h-3.5 text-status-err" />
<span>Fehler</span>
</>
)}
</div>
) : (
/* Standalone Realtime Status Indicator for Todos / Hashtags */
(() => {
@@ -1568,18 +1478,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onSelectTag={handleSelectTag}
onWikiLinkClick={handleWikiLinkClick}
onBackMobile={() => setMobileShowDetail(false)}
onSyncStatusChange={setActiveDocSyncStatus}
onSaveStatusChange={(status, err) => {
setDocSaveStatus(status);
setDocSaveError(err || null);
}}
onLiveStateChange={(state, initiator) => {
setDocLiveState(state);
setDocLiveInitiator(initiator || null);
}}
registerLiveToggle={(handler) => {
liveToggleRef.current = handler;
}}
/>
) : subView === 'tags' ? (
/* =========================================================================
@@ -2535,14 +2437,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
shares={unifiedShares}
teams={teams}
collaborators={collaborators}
allowRoleSelection={!shareModalDoc}
onAddShare={async (targetType, targetId, permission) => {
const perm = permission === 'editor' ? 'write' : 'read';
const perm = (!shareModalDoc && permission === 'editor') ? 'write' : 'read';
if (shareModalDoc) {
await api.createDocShare(
shareModalDoc.id,
targetType === 'team'
? { teamId: targetId, permission: perm }
: { userId: targetId, permission: perm }
? { teamId: targetId, permission: 'read' }
: { userId: targetId, permission: 'read' }
);
const updatedShares = await api.getDocShares(shareModalDoc.id);
setActiveDocShares(updatedShares);
@@ -2569,60 +2472,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}}
/>
)}
{/* WebSocket Blocked Modal */}
{showWsBlockedModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-150">
<div className="w-full max-w-md bg-canvas border border-border rounded-card shadow-modal p-6 space-y-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-status-err/15 flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5 text-status-err" />
</div>
<div className="flex-1">
<h3 className="text-sm font-bold text-main">
WebSockets in diesem Netzwerk blockiert
</h3>
<p className="text-xs text-muted mt-1 leading-relaxed">
Deine Firewall oder dein Firmen-Proxy blockiert WebSocket-Verbindungen (<code className="text-accent bg-panel px-1 py-0.5 rounded">ws://</code> / <code className="text-accent bg-panel px-1 py-0.5 rounded">wss://</code>).
</p>
</div>
</div>
<div className="bg-surface-subtle border border-border rounded-btn p-3 text-xs space-y-2 text-main">
<div className="flex items-center gap-2 text-status-ok font-semibold">
<Check className="w-4 h-4 shrink-0" />
<span>Kein Datenverlust: Auto-Save per HTTPS ist aktiv!</span>
</div>
<p className="text-muted leading-relaxed">
Jede getippte Zeile und alle Änderungen werden zuverlässig über Standard-HTTPS-Anfragen im Hintergrund gespeichert.
</p>
<p className="text-muted text-[11px] leading-relaxed">
Lediglich die gleichzeitige Tastendruck-Echtzeitanzeige mit anderen Nutzern im selben Moment ist ohne WebSockets eingeschränkt.
</p>
</div>
<div className="flex justify-between items-center pt-2">
<button
type="button"
onClick={() => {
liveToggleRef.current?.();
setShowWsBlockedModal(false);
}}
className="px-3 py-1.5 text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 rounded-btn transition-colors cursor-pointer"
>
Live-Sitzung beenden
</button>
<button
type="button"
onClick={() => setShowWsBlockedModal(false)}
className="btn-action px-4 py-1.5 text-xs font-semibold bg-accent text-on-accent hover:opacity-90 transition-opacity cursor-pointer"
>
Verstanden
</button>
</div>
</div>
</div>
)}
</div>
</div>
);