From 737441fe0086cb76da1ad4841c27829fc30da74d Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 12 May 2026 17:49:39 +0200
Subject: [PATCH] fixed teams
---
b-reference/teams-bot/architecture.md | 80 ++++++++++++++++++++++++++-
c-work/_CHANGELOG.md | 8 +++
2 files changed, 87 insertions(+), 1 deletion(-)
diff --git a/b-reference/teams-bot/architecture.md b/b-reference/teams-bot/architecture.md
index d617bbf..bc10ee2 100644
--- a/b-reference/teams-bot/architecture.md
+++ b/b-reference/teams-bot/architecture.md
@@ -1,6 +1,6 @@
-
+
# Teams Meeting Bot -- Architektur
@@ -99,6 +99,84 @@ Es wird **keine Label-Filterung** angewendet — die Tracks haben je nach Sessio
Refs: `service-teams-browser-bot/src/bot/audioCaptureProcedure.ts` (`__audioCaptureEnabled`, `__audioCaptureAttachTrack`, `startCapture`); Aufrufer `orchestrator.ts` (`_attemptJoin` STEP 4 → `_setState('in_meeting')` → `_enableTranscriptCapture`).
+### Chat-Panel-Toggle-Auswahl im Auth-Layout
+
+Im authentifizierten Full-Teams-Layout existieren ZWEI sichtbare Buttons mit "Chat" im aria-label:
+
+1. der **echte Toggle**: UUID-id, `aria-label="Chat (Ctrl+Shift+2)"`, **`aria-pressed="true|false"`** — toggelt das Meeting-Chat-Side-Panel.
+2. ein **Schein-Toggle**: `id="chat-button"`, `aria-label="Chat"`, **kein `aria-pressed`** — vermutlich der Side-Nav-Eintrag der Chat-App; klicken hat keinerlei Wirkung auf das Meeting-Chat-Panel.
+
+In light-meetings (anon) existiert nur `#chat-button` und der IST der echte Toggle (light-meetings nutzt menu-button-Semantik ohne `aria-pressed`).
+
+`_openChatPanel()` löst das so:
+
+- Sammelt alle sichtbaren `button` / `[role=button]` / `[role=menuitem]` deren `id`/`data-tid`/`aria-label`/`title` einen der Hints `chat`/`unterhalt`/`besprechung`/`conversation` enthält (sprachunabhängig).
+- **Bevorzugt Kandidaten mit `aria-pressed` ∈ {`true`,`false`}** (echte Toggle-Buttons), Fallback ist der erste passende Nicht-Toggle.
+- Trackt geklickte Buttons per `id|data-tid|aria-label`-Key und überspringt sie in den nächsten Runden — verhindert die Endlosschleife "klicke 12× denselben falschen Button" wenn der erste Pick nichts bewirkt.
+
+Refs: `service-teams-browser-bot/src/bot/chatProcedure.ts._openChatPanel`.
+
+### Chat-Panel-Detection
+
+`_isChatPanelOpen()` macht **zwei** Checks am `[data-tid="calling-right-side-panel"]`-Container — keine Fallbacks:
+
+1. **Existenz + echte Visibility** des Side-Pane-Containers: `offsetWidth/Height > 0 && offsetParent !== null`. Wenn das Panel zu ist, ist der Container entweder unmounted oder vom Parent auf 0×0 kollabiert.
+2. **Mode-Disambiguation per chat-spezifischen Child-Tids** innerhalb des sichtbaren Containers: `message-pane-layout`, `message-pane-body`, `chat-pane-compose-message-footer`, `message-pane-footer`, `#chat-pane-list`, `[data-app-name="chats"]`. Trennt Chat sauber von People / Info / Captions ohne Text-Lookup.
+
+Frühere Iterationen hatten zwei zusätzliche Schichten (aria-pressed-Toggle + Compose-Box-Sichtbarkeit) als "Fallback für ältere Auth-Layouts". Diese sind 2026-05-12 entfernt worden — sowohl anon als auch auth nutzen denselben `calling-right-side-panel`-Container, die Fallbacks hätten nie gefeuert (oder wären redundant gewesen wenn doch). Konform zur Coding-Regel "Do not add fallback code, if not necessary".
+
+#### Trapdoor: `vdi-occlusion`
+
+Die CSS-Klasse `vdi-occlusion` taucht in der neuen Calling-Layout-Version **als Permanent-Klasse** auf `calling-right-side-panel` UND `message-pane-layout` auf — auch wenn das Panel sichtbar geöffnet ist. Sie ist KEIN Visibility-Indikator. Ein älterer Detection-Code, der `vdi-occlusion → "Panel zu"` ableitete, lieferte deshalb **immer** false-negative bei offenem Panel → periodischer Scan triggerte `_openChatPanel()` → klickte den Toggle → schloss das Panel echt → klickte weitere chat-aria Buttons (Side-Nav-Apps "Chats"/"Meeting chats", Tab-Items `tab-item-com.microsoft.chattabs.*`) → Endlos-Toggle-Loop mit `UPR`-Page-Errors von Teams. Visibility wird ausschließlich über `offsetWidth/Height > 0 && (offsetParent !== null || position === fixed)` geprüft.
+
+#### Toggle-Button selbst trägt KEINE State-Information
+
+Im neuen Calling-Layout ist der `` byte-identisch in beiden Zuständen — kein `aria-pressed`, kein `data-state`, identische CSS-Klassen. Die optische "blaue Linie unten" beim offenen Zustand kommt aus einem CSS-`:has()`-Selektor der vom Vorhandensein des offenen Side-Panels abhängt, nicht vom Button-State. Detection muss daher zwingend über den Container, nicht den Button laufen.
+
+Refs: `service-teams-browser-bot/src/bot/chatProcedure.ts._isChatPanelOpen`.
+
+### Browser-Channel: echtes Chrome für anonyme Joins
+
+Anonyme Joins **müssen** mit dem lokal installierten echten Chrome laufen (`channel: 'chrome'`), nicht mit Playwrights gebündeltem Chromium. Teams' `light-meetings`-Pfad fingerprintet den Browser (vermutlich `Sec-CH-UA` Client-Hints + Canvas/WebGL-Signale + fehlende Chrome-Marken-Identität); das Bundled-Chromium fällt durch und Teams reagiert mit zwei Konsequenzen:
+
+1. Der anonyme Bot wird **trotz "Jeder umgeht die Lobby"-Setting** in die Lobby gezwungen.
+2. Während dieser Lobby-Phase läuft ein Preheating-PC-Codepfad, der mit `Cannot read properties of null (reading 'rejectMediaDescriptionsUpdateAsync')` crasht und den Bot auf die "Sorry, we couldn't connect you"-Seite wirft.
+
+Der **manuelle** Inkognito-Test im echten Chrome derselben Maschine geht beide Symptome nicht — er joint ohne Lobby. Das beweist, dass es kein generelles Teams-Problem für anonyme Gäste ist, sondern den Playwright-Chromium-Fingerprint trifft.
+
+Der **authentifizierte Pfad** funktioniert auch mit Bundled-Chromium, weil eine echte MS-Login-Session den Anti-Bot-Check überspringt.
+
+Konfiguration: `BOT_BROWSER_CHANNEL=chrome` in `.env`, gelesen in `src/config.ts` als `botBrowserChannel`, an `chromium.launch({ ..., channel })` durchgereicht in `src/bot/orchestrator.ts._launchBrowser()`. Leer = Bundled Chromium (Default, geeignet für Auth-Joins / Tests). `chrome` und `msedge` setzen den lokalen Real-Browser ein; auf den Bot-Hosts muss Chrome bzw. Edge installiert sein (auf den Standard-Chrome-Pfaden, die Playwright kennt).
+
+**Container-Deployment**: Das Microsoft Playwright Base-Image (`mcr.microsoft.com/playwright:v1.50.0-jammy`) liefert nur das gebündelte Chromium mit. Das `Dockerfile` installiert deshalb explizit das echte Google Chrome via `npx playwright install --with-deps chrome` (Playwright legt Chrome an einen ihm bekannten Pfad ab + apt-installiert alle System-Deps); `docker-compose.yml` setzt `BOT_BROWSER_CHANNEL=chrome` als Default-Env. Auf reinen VMs reicht ein OS-natives `apt-get install -y google-chrome-stable` aus dem Google-Repo, sofern die Chrome-Binary unter `/usr/bin/google-chrome[-stable]` landet.
+
+Bisect-Historie 2026-05-12 (Diagnose-Bypässe bleiben als undokumentierte Debug-Schalter im Code, falls erneut nötig):
+
+- `BOT_DISABLE_MEDIA_WRAPPERS=true` → Crash blieb. ⇒ Wrapper unschuldig.
+- `BOT_ANON_USE_AUTH_BROWSER_SETUP=true` (= minimale Chromium-Args + kein Stealth-Init) → Crash blieb. ⇒ weder Anon-Args noch Stealth-Properties sind die Ursache.
+- `BOT_BROWSER_CHANNEL=chrome` → Lobby weg, Crash weg, Audio-Capture läuft sauber. ⇒ root cause ist der Browser-Fingerprint.
+
+### Statisches Avatar-Video (ersetzt grünen Lade-Spinner)
+
+Wenn der Bot **kein** Video sendet, rendert Teams für andere Teilnehmer einen eigenen Platzhalter — eine grüne Fläche mit Initialen + Lade-Spinner, die nie zur Ruhe kommt. Mit `BOT_USE_CANVAS_VIDEO=true` schiebt der Bot stattdessen einen ruhigen, statischen Canvas-Stream als Video-Track in die WebRTC-Sender — eine einfarbige Fläche mit dem Anzeigenamen mittig, ohne Animation, ohne Branding.
+
+Konfiguration (alle drei Env-Variablen optional, alles in `src/config.ts`):
+
+| Env | Default | Bedeutung |
+|---|---|---|
+| `BOT_USE_CANVAS_VIDEO` | `false` | Schaltet das statische Avatar-Video ein. Empfohlen für den anonymen Bot. |
+| `BOT_AVATAR_BG_COLOR` | `#a8d4f0` (hellblau) | CSS-Farbe der Hintergrundfläche. |
+| `BOT_AVATAR_TEXT_COLOR` | `#1a3552` (dunkelblau) | CSS-Farbe des zentrierten Anzeigenamens. |
+
+Implementierung in `src/bot/mediaGetUserMediaPatch.ts`:
+
+- Canvas wird einmalig erzeugt (640×360), `draw()` zeichnet bei jedem Tick **identische** Pixel — keine Animation, kein Pulse, kein Gradient.
+- Tick-Rate auf 2 fps gesenkt (vorher 15 fps Animation). Der Tick bleibt nötig, weil `captureStream()` in headless Chromium den Track sonst pausiert; Inhalt ist aber konstant, daher entstehen keine Frame-Diffs auf dem Encoder.
+- Video-Track-`contentHint` von `'motion'` auf `'detail'` umgestellt — sagt dem WebRTC-Encoder "statisch", er reduziert Bitrate und behält Textschärfe.
+- Farben werden vom `audioProcedure.ts` an den Init-Script-Payload durchgereicht und in `_startBotAvatarStream()` als `fillStyle` für Hintergrund + Label verwendet.
+
+Der Auth-Bot ist von der Änderung nur betroffen, wenn `BOT_USE_CANVAS_VIDEO=true` gesetzt ist; sonst sendet er weiterhin gar kein Video.
+
## Hybrid-Routing: SPEECH_TEAMS + Agent
Der Teamsbot läuft auf zwei kooperierenden Pfaden:
diff --git a/c-work/_CHANGELOG.md b/c-work/_CHANGELOG.md
index ffb3145..a7faf81 100644
--- a/c-work/_CHANGELOG.md
+++ b/c-work/_CHANGELOG.md
@@ -14,6 +14,14 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-05-12
+- 2026-05-12 | refactor | teams-bot | Chat-Panel-Detection: tote Fallback-Schichten entfernt. `_isChatPanelOpen()` reduziert auf den nachweislich funktionierenden Pfad: `[data-tid="calling-right-side-panel"]` Existenz+Visibility + chat-spezifischer Child-`data-tid` darin. Schicht 2 (`aria-pressed`-Toggle für "ältere Auth-Layouts") und Schicht 3 (Compose-Box-Sichtbarkeit als Last-Resort) waren toter Code — anon und auth nutzen beide denselben `calling-right-side-panel`-Container, der `aria-pressed`-Pfad war zwei Iterationen vorher selbst als unzuverlässig verworfen worden. Konform mit "Do not add fallback code, if not necessary". (b-ref: teams-bot/architecture.md → "Chat-Panel-Detection")
+- 2026-05-12 | feat | teams-bot | Statisches Avatar-Video für anonymen Bot ersetzt Teams' grünen "no video"-Spinner. Neuer Toggle `BOT_USE_CANVAS_VIDEO=true` (Default true im `.env`) plus `BOT_AVATAR_BG_COLOR=#a8d4f0` / `BOT_AVATAR_TEXT_COLOR=#1a3552` in `src/config.ts`, durchgereicht via `audioProcedure.ts` an den Init-Script-Payload `mediaGetUserMediaPatch.ts`. `_startBotAvatarStream()` zeichnet jetzt eine ruhige einfarbige Fläche + zentrierter Anzeigename (kein Pulse, kein Gradient, keine "PORTA"-Marke), Tick-Rate von 15 fps auf 2 fps gesenkt (genug um `captureStream()` in headless Chromium aktiv zu halten, ohne dass der Encoder Frame-Diffs sieht), `contentHint='motion'` → `'detail'` damit WebRTC für statisches Bild Bitrate spart und Text scharf bleibt. Auth-Bot ist nur betroffen wenn der Toggle aktiv ist. (b-ref: teams-bot/architecture.md → "Statisches Avatar-Video (ersetzt grünen Lade-Spinner)")
+- 2026-05-12 | fix | frontend-nyla | TeamsBot Stop-Button Sichtbarkeit: Logik von Whitelist (`['active','joining','pending']`) auf Blacklist (`!['ended','error','leaving']`) umgestellt in `TeamsbotSessionView.tsx` Z.841 + `TeamsbotDashboardView.tsx` Z.264. Verhindert Verschwinden des Buttons bei kurzfristig leerem/unbekanntem Status (z.B. SSE-Race kurz nach Mount, neuer Backend-Status). Button verschwindet nur noch nach explizit terminalen States. Hinweis zur i18n: Source-Keys `t('Sitzung beenden')` / `t('Stoppen')` waren bereits korrekt verpackt — die User-sichtbare englische Anzeige "End Session" stammt aus der Übersetzungs-DB und ist die intendierte i18n-Auflösung des deutschen Source-Keys.
+- 2026-05-12 | fix | teams-bot | Chat-Panel-Toggle-Auswahl im Auth-Layout: `chatProcedure._openChatPanel()` neu implementiert. Auth-Full-Teams hat zwei "Chat"-Buttons — einen echten Toggle (UUID-id, `aria="Chat (Ctrl+Shift+2)"`, `aria-pressed="false"`) und einen Schein-Button `#chat-button` ohne `aria-pressed`, der nicht togglt. Vorher: Selector-Liste nahm `#chat-button` als ersten Treffer und klickte 12× denselben falschen Button. Neu: alle visibility-gefilterten Chat-Hint-Kandidaten in Page-Evaluate gesammelt, Toggle-Buttons (mit `aria-pressed`) bevorzugt, geklickte Keys getrackt — derselbe Button wird nie zweimal geklickt, Loop wechselt zum nächsten Kandidaten. Sprachunabhängig (DE/EN-Hints `chat`/`unterhalt`/`besprechung`/`conversation`). (b-ref: teams-bot/architecture.md → "Chat-Panel-Toggle-Auswahl im Auth-Layout")
+- 2026-05-12 | fix | teams-bot | Chat-Panel Detection — `vdi-occlusion`-Trapdoor entschärft + Side-Pane-Container als primäre Quelle. Befund am DOM (User-Inspect): die Klasse `vdi-occlusion` ist im neuen Calling-Layout **Permanent-Marker** auf `calling-right-side-panel` und `message-pane-layout` — auch bei sichtbar offenem Panel. Mein vorheriger Check "vdi-occlusion → zu" war daher immer false-negative → Toggle-Loop. Der `` selbst ist byte-identisch in beiden States (kein `aria-pressed`, kein `data-state`, blaue Linie kommt aus CSS-`:has()` auf dem Side-Panel). Detection neu: Schicht 1 prüft `[data-tid="calling-right-side-panel"]` Existenz + offsetWidth/Height + offsetParent (echtes Visibility-Signal), und Mode-Disambiguation per chat-spezifischen Tids drinnen (`message-pane-layout`, `chat-pane-compose-message-footer`, `#chat-pane-list`, `[data-app-name="chats"]` u.a.) — sprachunabhängig, trennt Chat von People/Info ohne Text-Lookup. Schicht 2 (aria-pressed Toggle) bleibt für legacy-Auth-Layouts. (b-ref: teams-bot/architecture.md → "Chat-Panel-Detection — Schicht-Strategie")
+- 2026-05-12 | fix | teams-bot | Chat-Panel Detection (zwischenzeitlich) — Schicht-Strategie via aria-pressed: `chatProcedure._isChatPanelOpen()` umgestellt auf "echter Toggle-Button" (chat-Hint im aria-label UND `aria-pressed` ∈ {true,false}, sprachunabhängig) und liest dessen `aria-pressed` als Quelle der Wahrheit — fixt den Auth-Toggle-Loop. Im Auth-Layout existieren mehrere "chat"-aria Buttons (Side-Nav-Einträge "Chats", "Meeting chats", Tab-Items `tab-item-com.microsoft.chattabs.*`), aber nur der echte Meeting-Chat-Toggle hat `aria-pressed`. Vorher prüften wir hartcodiert `#chat-button` (= Side-Nav-App in Auth, NICHT der Toggle), bekamen False-Negative "Panel zu" obwohl offen → periodischer Scan klickte den echten Toggle → schloss Panel → klickte weitere Buttons → Endlos-Toggle-Loop mit `UPR`-Page-Errors. Light-meetings-Pfad (`message-pane-layout` mit `vdi-occlusion`-Klasse oder `offsetHeight=0`) bleibt als Schicht 2 für Anon (kein `aria-pressed`-Toggle vorhanden). (b-ref: teams-bot/architecture.md → "Chat-Panel-Detection — Schicht-Strategie")
+- 2026-05-12 | build | teams-bot | Container-Image installiert echtes Chrome: `Dockerfile` ergänzt um `RUN npx playwright install --with-deps chrome` (lädt Google Chrome stable + alle apt-Deps an Playwright-bekannten Pfad) und `ENV BOT_BROWSER_CHANNEL=chrome`. `docker-compose.yml` setzt `BOT_BROWSER_CHANNEL=${BOT_BROWSER_CHANNEL:-chrome}` als Default. Macht den anon-Bot-Fix container-deploy-fähig. (b-ref: teams-bot/architecture.md → "Container-Deployment")
+- 2026-05-12 | fix | teams-bot | Anonymer Bot: Browser-Channel auf echtes Chrome umgestellt (`BOT_BROWSER_CHANNEL=chrome`). Playwrights gebündeltes Chromium wird von Teams' `light-meetings`-Pfad als Automation gefingerprintet → erzwungene Lobby + Preheating-Crash (`rejectMediaDescriptionsUpdateAsync`) → "Sorry, we couldn't connect you". Mit lokal installiertem echten Chrome geht der Bot direkt ohne Lobby ins Meeting, kein Crash. Neu: `botBrowserChannel` in `config.ts`, an `chromium.launch({ channel })` in `orchestrator._launchBrowser()` durchgereicht. Bisect bestätigt: weder Media-Wrapper (`BOT_DISABLE_MEDIA_WRAPPERS`) noch Anon-spezifische Chromium-Args / Stealth-Init (`BOT_ANON_USE_AUTH_BROWSER_SETUP`) waren die Ursache; beide Debug-Schalter bleiben als Bisect-Tools im Code. Auth-Joins funktionieren auch mit Bundled-Chromium (echte MS-Session überspringt den Check). (b-ref: teams-bot/architecture.md → "Browser-Channel: echtes Chrome für anonyme Joins")
- 2026-05-12 | fix | teams-bot | TeamsBot Chat-Scraper: Container-Fallback für light-meetings DOM-Branche. **`chatProcedure.ts`** periodischer Scan promoviert `target` zu `document` wenn der Chat-Container (`message-pane-layout`) `offsetHeight=0` hat ODER Selectoren 0 Treffer liefern, aber global `[class*="fui-ChatMessage"]` existiert (light-meetings rendert Chat-Bubbles in paralleler DOM-Branche). Zusätzlich Root-Cause-Guard `if (!text && author !== 'Unknown')` in beiden Pfaden (MutationObserver Strategy 1 + periodischer Scan) restauriert — verhindert dass strukturelle Fragmente ohne Author als `Unknown: 22:04` Transcripts durchsickern. (c-work: 4-done/2026-04-teamsbot-greenfield-ia-and-live-update.md)
- 2026-05-12 | fix | teams-bot | Anonymer Bot: Audio-Capture WebRTC-Wrapper greift nicht mehr in Teams' Pre-Join/Lobby-SDP-Zyklen ein (Ursache des `rejectMediaDescriptionsUpdateAsync`-Crashes und der Lobby-Hänger). `audioCaptureProcedure.ts`: neues `__audioCaptureEnabled`-Flag (default false), `track`-Handler im Wrapper macht nur noch DIAG-Log solange Flag false ist; `__audioCaptureAttachTrack` als window-exponierter Builder; `startCapture()` setzt Flag erst nach `_setState('in_meeting')` und iteriert `getReceivers()` aller `connected` PCs für bereits existierende Audio-Spuren. Label-Filter `mainAudio-*` entfernt (war falsch generalisiert — manche Sessions haben nur eine PC mit UUID-Label). (c-work: 4-done/2026-04-teamsbot-greenfield-ia-and-live-update.md)
- 2026-05-12 | feat | frontend-nyla, gateway | FormGeneratorTable Grouping Refactor: Default auf Inline-Modus umgestellt; Toggle-Icon (Inline/Sections) in TableViewsBar; Sections-Mode Multi-Level (alle Permutationen als flache Sections); pageSizeOptions um 1000/2000/10000 erweitert; Collapse-All/Expand-All Buttons fuer Inline-Grouping; 4 Pages (Connections, Prompts, Files, BillingData) auf Inline-Default umgestellt.