diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index aca4d71..4ba16c9 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -115,6 +115,31 @@ export interface VoiceOption { naturalSampleRateHertz: number; } +// Auth Detection Test Types +export interface AuthTestResult { + variantId: string; + variantName: string; + success: boolean; + pageType: 'v2' | 'lightMeetings' | 'error' | 'unknown'; + finalUrl: string; + hasSignInLink: boolean; + hasNameInput: boolean; + hasJoinButton: boolean; + authAttempted: boolean; + authSuccess: boolean | null; + screenshot?: string; + durationMs: number; + error?: string; + detectedSignals: string[]; +} + +export interface AuthTestResults { + meetingUrl: string; + timestamp: string; + variants: AuthTestResult[]; + recommendation: string; +} + // SSE Event Types export interface TeamsbotSSEEvent { type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState'; @@ -281,6 +306,19 @@ export async function fetchVoices(languageCode: string): Promise } } +/** + * Run auth detection tests against a Teams meeting URL. + * Tests 5 browser configuration variants to determine which ones + * receive the /v2/ (authenticated) vs light-meetings (anonymous) page. + * Does NOT join the meeting. + */ +export async function testAuth(instanceId: string, meetingUrl: string): Promise { + const response = await api.post(`/api/teamsbot/${instanceId}/test-auth`, { meetingUrl }, { + timeout: 300000, // 5 minutes — tests run sequentially + }); + return response.data; +} + /** * Create an SSE EventSource for live session streaming. * Returns the EventSource instance for the caller to manage. diff --git a/src/pages/views/teamsbot/Teamsbot.module.css b/src/pages/views/teamsbot/Teamsbot.module.css index deab067..cfc6997 100644 --- a/src/pages/views/teamsbot/Teamsbot.module.css +++ b/src/pages/views/teamsbot/Teamsbot.module.css @@ -495,3 +495,247 @@ justify-content: flex-end; padding-top: 1rem; } + +/* ============================================================================ + Auth Detection Test Section + ============================================================================ */ + +.testSection { + margin-bottom: 2rem; + padding: 1.25rem; + border: 1px solid var(--primary-color, #4A90D9); + border-radius: 8px; + background: rgba(74, 144, 217, 0.04); +} + +.testSectionHeader { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.testSectionTitle { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--primary-color, #4A90D9); +} + +.testInputRow { + display: flex; + gap: 0.5rem; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.testInputRow .input { + flex: 1; +} + +.testButton { + padding: 0.5rem 1.25rem; + background: var(--primary-color, #4A90D9); + color: #fff; + border: none; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + display: flex; + align-items: center; + gap: 0.5rem; + transition: background 0.2s; +} + +.testButton:hover { + background: var(--primary-hover, #3A7BC8); +} + +.testButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.testProgress { + font-size: 0.85rem; + color: var(--text-secondary, #666); + padding: 0.75rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.testResultsTable { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + margin-top: 0.75rem; +} + +.testResultsTable th { + text-align: left; + padding: 0.5rem 0.75rem; + border-bottom: 2px solid var(--border-color, #e0e0e0); + font-weight: 600; + font-size: 0.8rem; + color: var(--text-secondary, #666); + white-space: nowrap; +} + +.testResultsTable td { + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); + vertical-align: middle; +} + +.testResultsTable tr:last-child td { + border-bottom: none; +} + +.testBadgeV2 { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 600; + background: rgba(74, 217, 154, 0.15); + color: #2D8E5C; +} + +.testBadgeLightMeetings { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 600; + background: rgba(217, 168, 74, 0.15); + color: #B8860B; +} + +.testBadgeError { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 600; + background: rgba(217, 74, 74, 0.15); + color: #D94A4A; +} + +.testBadgeUnknown { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 600; + background: rgba(128, 128, 128, 0.15); + color: #666; +} + +.testCheckmark { + color: #2D8E5C; + font-weight: 600; +} + +.testCross { + color: #D94A4A; + font-weight: 600; +} + +.testDash { + color: var(--text-tertiary, #999); +} + +.testRecommendation { + margin-top: 0.75rem; + padding: 0.75rem 1rem; + background: rgba(74, 144, 217, 0.08); + border-radius: 6px; + font-size: 0.85rem; + line-height: 1.5; + color: var(--text-primary, #333); + border-left: 3px solid var(--primary-color, #4A90D9); +} + +.testScreenshotButton { + padding: 0.15rem 0.5rem; + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 4px; + background: var(--surface-color, #fff); + color: var(--primary-color, #4A90D9); + font-size: 0.75rem; + cursor: pointer; + white-space: nowrap; +} + +.testScreenshotButton:hover { + background: var(--surface-alt, #f5f5f5); +} + +.testScreenshotOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + cursor: pointer; +} + +.testScreenshotImage { + max-width: 90vw; + max-height: 85vh; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.testScreenshotCaption { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + color: #fff; + font-size: 0.9rem; + background: rgba(0, 0, 0, 0.6); + padding: 0.5rem 1rem; + border-radius: 6px; +} + +.testErrorText { + font-size: 0.75rem; + color: var(--danger-color, #D94A4A); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.testVariantName { + font-weight: 500; +} + +.testDuration { + font-size: 0.8rem; + color: var(--text-tertiary, #999); + white-space: nowrap; +} + +.testSignals { + font-size: 0.75rem; + color: var(--text-tertiary, #999); + max-width: 250px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + animation: spin 1s linear infinite; +} diff --git a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx index c30cbf1..b64a427 100644 --- a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx +++ b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; -import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi'; -import { FaPlay, FaSpinner } from 'react-icons/fa'; +import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, AuthTestResults, AuthTestResult } from '../../../api/teamsbotApi'; +import { FaPlay, FaSpinner, FaFlask, FaImage } from 'react-icons/fa'; import styles from './Teamsbot.module.css'; /** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */ @@ -37,6 +37,13 @@ export const TeamsbotSettingsView: React.FC = () => { const [voices, setVoices] = useState([]); const [loadingVoices, setLoadingVoices] = useState(false); + // Auth detection test state + const [testMeetingUrl, setTestMeetingUrl] = useState(''); + const [testRunning, setTestRunning] = useState(false); + const [testResults, setTestResults] = useState(null); + const [testError, setTestError] = useState(null); + const [screenshotPreview, setScreenshotPreview] = useState<{ src: string; caption: string } | null>(null); + const _loadConfig = useCallback(async () => { if (!instanceId) return; try { @@ -142,6 +149,46 @@ export const TeamsbotSettingsView: React.FC = () => { } }; + const _handleRunAuthTest = async () => { + if (!instanceId || !testMeetingUrl.trim()) return; + setTestRunning(true); + setTestResults(null); + setTestError(null); + + try { + const results = await teamsbotApi.testAuth(instanceId, testMeetingUrl.trim()); + setTestResults(results); + } catch (err: any) { + setTestError(err.message || 'Auth-Test fehlgeschlagen'); + } finally { + setTestRunning(false); + } + }; + + const _getPageTypeBadge = (result: AuthTestResult) => { + switch (result.pageType) { + case 'v2': + return /v2/; + case 'lightMeetings': + return light-meetings; + case 'error': + return Fehler; + default: + return Unbekannt; + } + }; + + const _getCheckmark = (value: boolean) => { + return value + ? + : ; + }; + + const _formatDuration = (ms: number) => { + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; + }; + if (loading) return
Lade Konfiguration...
; return ( @@ -152,6 +199,127 @@ export const TeamsbotSettingsView: React.FC = () => { {error &&
{error}
} {successMsg &&
{successMsg}
} + {/* Auth Detection Test */} +
+
+ +

Auth-Erkennung testen

+
+ + Testet 5 Browser-Konfigurationen gegen einen aktiven Teams-Call. + Prueft ob Teams /v2/ (Auth moeglich) oder light-meetings (anonym erzwungen) liefert. + Der Bot tritt dem Meeting NICHT bei. + + +
+ setTestMeetingUrl(e.target.value)} + placeholder="https://teams.microsoft.com/meet/..." + disabled={testRunning} + /> + +
+ + {testRunning && ( +
+ + 5 Varianten werden sequentiell getestet (~2-3 Minuten)... +
+ )} + + {testError &&
{testError}
} + + {testResults && ( + <> + + + + + + + + + + + + + + + {testResults.variants.map((result) => ( + + + + + + + + + + + ))} + +
VarianteSeiteSign-inName-InputJoinAuthDauer
+ {result.variantName} + {result.error && ( +
+ {result.error} +
+ )} +
{_getPageTypeBadge(result)}{result.success ? _getCheckmark(result.hasSignInLink) : }{result.success ? _getCheckmark(result.hasNameInput) : }{result.success ? _getCheckmark(result.hasJoinButton) : } + {result.authAttempted + ? (result.authSuccess ? Erfolgreich : Fehlgeschlagen) + : + } + {_formatDuration(result.durationMs)} + {result.screenshot && ( + + )} +
+ + {testResults.recommendation && ( +
+ Empfehlung: {testResults.recommendation} +
+ )} + + )} +
+ + {/* Screenshot Overlay */} + {screenshotPreview && ( +
setScreenshotPreview(null)} + > + {screenshotPreview.caption} +
+ {screenshotPreview.caption} — Klicken zum Schliessen +
+
+ )} + {/* Bot Identity */}

Bot-Identitaet