feat(teamsbot): show per-variant logs in auth test UI, single direct /v2/ test

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-16 23:06:55 +01:00
parent ae18912e1e
commit 2f9de759eb
3 changed files with 149 additions and 36 deletions

View file

@ -131,6 +131,7 @@ export interface AuthTestResult {
durationMs: number; durationMs: number;
error?: string; error?: string;
detectedSignals: string[]; detectedSignals: string[];
logs: string[];
} }
export interface AuthTestResults { export interface AuthTestResults {

View file

@ -732,6 +732,69 @@
max-width: 250px; max-width: 250px;
} }
/* Variant cell with expand toggle */
.testVariantCell {
display: flex;
align-items: center;
gap: 0.35rem;
}
.testLogToggle {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
padding: 0.15rem;
font-size: 0.7rem;
color: var(--text-secondary, #666);
border-radius: 3px;
flex-shrink: 0;
}
.testLogToggle:hover {
background: var(--surface-alt, #f5f5f5);
color: var(--primary-color, #4A90D9);
}
/* Log row */
.testLogRow td {
padding: 0 !important;
border-top: none !important;
}
.testLogContainer {
background: var(--surface-dark, #1e1e1e);
color: var(--text-light, #d4d4d4);
font-family: 'Fira Code', 'Consolas', 'Courier New', monospace;
font-size: 0.72rem;
line-height: 1.5;
padding: 0.5rem 0.75rem;
max-height: 200px;
overflow-y: auto;
border-radius: 0 0 4px 4px;
margin: 0 0.5rem 0.5rem;
}
.testLogLine {
white-space: pre-wrap;
word-break: break-all;
padding: 0.05rem 0;
}
.testLogInfo {
color: var(--text-light, #d4d4d4);
}
.testLogWarn {
color: #e5a100;
}
.testLogError {
color: #f14c4c;
}
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import * as teamsbotApi from '../../../api/teamsbotApi'; import * as teamsbotApi from '../../../api/teamsbotApi';
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, AuthTestResults, AuthTestResult } from '../../../api/teamsbotApi'; import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, AuthTestResults, AuthTestResult } from '../../../api/teamsbotApi';
import { FaPlay, FaSpinner, FaFlask, FaImage } from 'react-icons/fa'; import { FaPlay, FaSpinner, FaFlask, FaImage, FaChevronDown, FaChevronRight } from 'react-icons/fa';
import styles from './Teamsbot.module.css'; import styles from './Teamsbot.module.css';
/** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */ /** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */
@ -43,6 +43,7 @@ export const TeamsbotSettingsView: React.FC = () => {
const [testResults, setTestResults] = useState<AuthTestResults | null>(null); const [testResults, setTestResults] = useState<AuthTestResults | null>(null);
const [testError, setTestError] = useState<string | null>(null); const [testError, setTestError] = useState<string | null>(null);
const [screenshotPreview, setScreenshotPreview] = useState<{ src: string; caption: string } | null>(null); const [screenshotPreview, setScreenshotPreview] = useState<{ src: string; caption: string } | null>(null);
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
const _loadConfig = useCallback(async () => { const _loadConfig = useCallback(async () => {
if (!instanceId) return; if (!instanceId) return;
@ -189,6 +190,28 @@ export const TeamsbotSettingsView: React.FC = () => {
return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 1000).toFixed(1)}s`;
}; };
const _toggleLogs = (variantId: string) => {
setExpandedLogs(prev => {
const next = new Set(prev);
if (next.has(variantId)) {
next.delete(variantId);
} else {
next.add(variantId);
}
return next;
});
};
const _getLogLevelClass = (log: string): string => {
if (log.startsWith('[ERROR]') || log.startsWith('[CONSOLE:error]') || log.startsWith('[PAGE_ERROR]')) {
return styles.testLogError;
}
if (log.startsWith('[WARN]') || log.startsWith('[CONSOLE:warning]')) {
return styles.testLogWarn;
}
return styles.testLogInfo;
};
if (loading) return <div className={styles.loading}>Lade Konfiguration...</div>; if (loading) return <div className={styles.loading}>Lade Konfiguration...</div>;
return ( return (
@ -206,7 +229,7 @@ export const TeamsbotSettingsView: React.FC = () => {
<h4 className={styles.testSectionTitle}>Auth-Erkennung testen</h4> <h4 className={styles.testSectionTitle}>Auth-Erkennung testen</h4>
</div> </div>
<span className={styles.hint}> <span className={styles.hint}>
Testet 5 Browser-Konfigurationen gegen einen aktiven Teams-Call. Testet Direct-/v2/-Navigation gegen einen aktiven Teams-Call.
Prueft ob Teams /v2/ (Auth moeglich) oder light-meetings (anonym erzwungen) liefert. Prueft ob Teams /v2/ (Auth moeglich) oder light-meetings (anonym erzwungen) liefert.
Der Bot tritt dem Meeting NICHT bei. Der Bot tritt dem Meeting NICHT bei.
</span> </span>
@ -233,7 +256,7 @@ export const TeamsbotSettingsView: React.FC = () => {
{testRunning && ( {testRunning && (
<div className={styles.testProgress}> <div className={styles.testProgress}>
<FaSpinner className={styles.spinner} /> <FaSpinner className={styles.spinner} />
5 Varianten werden sequentiell getestet (~2-3 Minuten)... Test laeuft (~30-45 Sekunden)...
</div> </div>
)} )}
@ -256,40 +279,66 @@ export const TeamsbotSettingsView: React.FC = () => {
</thead> </thead>
<tbody> <tbody>
{testResults.variants.map((result) => ( {testResults.variants.map((result) => (
<tr key={result.variantId}> <React.Fragment key={result.variantId}>
<td> <tr>
<span className={styles.testVariantName}>{result.variantName}</span> <td>
{result.error && ( <div className={styles.testVariantCell}>
<div className={styles.testErrorText} title={result.error}> {result.logs && result.logs.length > 0 && (
{result.error} <button
className={styles.testLogToggle}
onClick={() => _toggleLogs(result.variantId)}
title="Logs anzeigen/ausblenden"
>
{expandedLogs.has(result.variantId) ? <FaChevronDown /> : <FaChevronRight />}
</button>
)}
<span className={styles.testVariantName}>{result.variantName}</span>
</div> </div>
)} {result.error && (
</td> <div className={styles.testErrorText} title={result.error}>
<td>{_getPageTypeBadge(result)}</td> {result.error.length > 120 ? result.error.substring(0, 120) + '...' : result.error}
<td>{result.success ? _getCheckmark(result.hasSignInLink) : <span className={styles.testDash}></span>}</td> </div>
<td>{result.success ? _getCheckmark(result.hasNameInput) : <span className={styles.testDash}></span>}</td> )}
<td>{result.success ? _getCheckmark(result.hasJoinButton) : <span className={styles.testDash}></span>}</td> </td>
<td> <td>{_getPageTypeBadge(result)}</td>
{result.authAttempted <td>{result.success ? _getCheckmark(result.hasSignInLink) : <span className={styles.testDash}></span>}</td>
? (result.authSuccess ? <span className={styles.testCheckmark}>Erfolgreich</span> : <span className={styles.testCross}>Fehlgeschlagen</span>) <td>{result.success ? _getCheckmark(result.hasNameInput) : <span className={styles.testDash}></span>}</td>
: <span className={styles.testDash}></span> <td>{result.success ? _getCheckmark(result.hasJoinButton) : <span className={styles.testDash}></span>}</td>
} <td>
</td> {result.authAttempted
<td><span className={styles.testDuration}>{_formatDuration(result.durationMs)}</span></td> ? (result.authSuccess ? <span className={styles.testCheckmark}>Erfolgreich</span> : <span className={styles.testCross}>Fehlgeschlagen</span>)
<td> : <span className={styles.testDash}></span>
{result.screenshot && ( }
<button </td>
className={styles.testScreenshotButton} <td><span className={styles.testDuration}>{_formatDuration(result.durationMs)}</span></td>
onClick={() => setScreenshotPreview({ <td>
src: `data:image/jpeg;base64,${result.screenshot}`, {result.screenshot && (
caption: result.variantName, <button
})} className={styles.testScreenshotButton}
> onClick={() => setScreenshotPreview({
<FaImage /> Bild src: `data:image/jpeg;base64,${result.screenshot}`,
</button> caption: result.variantName,
)} })}
</td> >
</tr> <FaImage /> Bild
</button>
)}
</td>
</tr>
{expandedLogs.has(result.variantId) && result.logs && result.logs.length > 0 && (
<tr className={styles.testLogRow}>
<td colSpan={8}>
<div className={styles.testLogContainer}>
{result.logs.map((log, idx) => (
<div key={idx} className={`${styles.testLogLine} ${_getLogLevelClass(log)}`}>
{log}
</div>
))}
</div>
</td>
</tr>
)}
</React.Fragment>
))} ))}
</tbody> </tbody>
</table> </table>