fix(teamsbot): always save user credentials before session start, fix SSE reconnect loop, add stop button for pending

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-03-01 09:26:46 +01:00
parent bc5bae7c34
commit 294710b415
2 changed files with 30 additions and 31 deletions

View file

@ -35,7 +35,6 @@ export const TeamsbotDashboardView: React.FC = () => {
const [showCredentialForm, setShowCredentialForm] = useState(false); const [showCredentialForm, setShowCredentialForm] = useState(false);
const [credEmail, setCredEmail] = useState(''); const [credEmail, setCredEmail] = useState('');
const [credPassword, setCredPassword] = useState(''); const [credPassword, setCredPassword] = useState('');
const [saveCredentials, setSaveCredentials] = useState(true);
const [savingCredentials, setSavingCredentials] = useState(false); const [savingCredentials, setSavingCredentials] = useState(false);
// MFA state // MFA state
@ -128,20 +127,18 @@ export const TeamsbotDashboardView: React.FC = () => {
return; return;
} }
// Save credentials if requested (first-time entry) // userAccount with new credentials: always save to DB (backend loads from there)
if (joinMode === 'userAccount' && showCredentialForm && credEmail && credPassword) { if (joinMode === 'userAccount' && showCredentialForm && credEmail && credPassword) {
if (saveCredentials) { try {
try { setSavingCredentials(true);
setSavingCredentials(true); await teamsbotApi.saveUserAccount(instanceId, credEmail, credPassword);
await teamsbotApi.saveUserAccount(instanceId, credEmail, credPassword); setUserAccount({ hasSavedCredentials: true, email: credEmail });
setUserAccount({ hasSavedCredentials: true, email: credEmail }); } catch (err: any) {
} catch (err: any) { setError(err.message || 'Fehler beim Speichern der Zugangsdaten');
setError(err.message || 'Fehler beim Speichern der Zugangsdaten'); setSavingCredentials(false);
setSavingCredentials(false); return;
return; } finally {
} finally { setSavingCredentials(false);
setSavingCredentials(false);
}
} }
setShowCredentialForm(false); setShowCredentialForm(false);
} }
@ -373,15 +370,9 @@ export const TeamsbotDashboardView: React.FC = () => {
disabled={savingCredentials} disabled={savingCredentials}
/> />
</div> </div>
<div className={styles.checkboxRow}> <span style={{ fontSize: '12px', color: '#888', marginTop: '4px', display: 'block' }}>
<input Zugangsdaten werden verschluesselt gespeichert.
type="checkbox" </span>
id="saveCredentials"
checked={saveCredentials}
onChange={(e) => setSaveCredentials(e.target.checked)}
/>
<label htmlFor="saveCredentials">Zugangsdaten speichern (verschluesselt)</label>
</div>
{userAccount?.hasSavedCredentials && ( {userAccount?.hasSavedCredentials && (
<button <button
className={styles.viewButton} className={styles.viewButton}

View file

@ -83,10 +83,16 @@ export const TeamsbotSessionView: React.FC = () => {
_loadSession(); _loadSession();
}, [_loadSession]); }, [_loadSession]);
// SSE Live Stream // SSE Live Stream - connect once per session, don't re-create on status changes
const sseSessionRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (!instanceId || !sessionId || !session) return; if (!instanceId || !sessionId || !session) return;
if (!['active', 'joining', 'pending'].includes(session.status)) return; if (!['active', 'joining', 'pending'].includes(session.status)) return;
// Avoid reconnecting if already streaming this session
if (sseSessionRef.current === sessionId && eventSourceRef.current) return;
eventSourceRef.current?.close();
sseSessionRef.current = sessionId;
const eventSource = teamsbotApi.createSessionStream(instanceId, sessionId); const eventSource = teamsbotApi.createSessionStream(instanceId, sessionId);
eventSourceRef.current = eventSource; eventSourceRef.current = eventSource;
@ -110,11 +116,12 @@ export const TeamsbotSessionView: React.FC = () => {
if (['ended', 'error'].includes(sseEvent.data.status)) { if (['ended', 'error'].includes(sseEvent.data.status)) {
setIsLive(false); setIsLive(false);
eventSource.close(); eventSource.close();
eventSourceRef.current = null;
sseSessionRef.current = null;
} }
break; break;
case 'analysis': case 'analysis':
// Debug info - could show in UI
break; break;
case 'ttsDeliveryStatus': { case 'ttsDeliveryStatus': {
@ -132,7 +139,6 @@ export const TeamsbotSessionView: React.FC = () => {
} }
case 'suggestedResponse': case 'suggestedResponse':
// Manual mode: show suggested response
break; break;
case 'ping': case 'ping':
@ -150,16 +156,18 @@ export const TeamsbotSessionView: React.FC = () => {
return () => { return () => {
eventSource.close(); eventSource.close();
eventSourceRef.current = null; eventSourceRef.current = null;
sseSessionRef.current = null;
setIsLive(false); setIsLive(false);
}; };
}, [instanceId, sessionId, session?.status]); }, [instanceId, sessionId]);
// Polling fallback: refresh session data every 5s when session is active (in case SSE fails) // Polling fallback: refresh session data every 5s when SSE is not connected
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const isActive = useMemo(() => session && ['pending', 'joining', 'active'].includes(session.status), [session]); const isActive = useMemo(() => session && ['pending', 'joining', 'active'].includes(session.status), [session]);
useEffect(() => { useEffect(() => {
if (isActive && instanceId && sessionId && !isLive) { if (instanceId && sessionId && (isActive || !session)) {
pollRef.current = setInterval(async () => { pollRef.current = setInterval(async () => {
if (isLive) return;
try { try {
const result = await teamsbotApi.getSession(instanceId, sessionId); const result = await teamsbotApi.getSession(instanceId, sessionId);
setSession(result.session); setSession(result.session);
@ -169,7 +177,7 @@ export const TeamsbotSessionView: React.FC = () => {
}, 5000); }, 5000);
} }
return () => { if (pollRef.current) clearInterval(pollRef.current); }; return () => { if (pollRef.current) clearInterval(pollRef.current); };
}, [isActive, instanceId, sessionId, isLive]); }, [isActive, instanceId, sessionId, isLive, session]);
// Auto-scroll transcript // Auto-scroll transcript
useEffect(() => { useEffect(() => {
@ -254,7 +262,7 @@ export const TeamsbotSessionView: React.FC = () => {
{isLive && <span className={styles.liveBadge}>LIVE</span>} {isLive && <span className={styles.liveBadge}>LIVE</span>}
</div> </div>
<div className={styles.sessionControls}> <div className={styles.sessionControls}>
{['active', 'joining'].includes(session.status) && ( {['active', 'joining', 'pending'].includes(session.status) && (
<button className={styles.stopButton} onClick={_handleStop}>Sitzung beenden</button> <button className={styles.stopButton} onClick={_handleStop}>Sitzung beenden</button>
)} )}
</div> </div>