Teams Web read — dual architecture (LCDL)

Consumer mirror: forge-cockpit-web teams-web-read-dual-architecture.md.

Updated

LCDL owns Playwright read atoms (evaluate, scroll, chunk harvest); Cockpit owns queue, SQLite, sync UI, dedupe, and unified_message; Edge owns operator session on https://teams.microsoft.com/v2/.

No EML / Downloads: Teams has no outlook_download_message_eml, no adopt_download_file, no x-token HTTP export path. List harvest is DOM preview only unless consumer adds a detail task loop.

Discovery strategies (consumer + LCDL)

Priority Strategy Governed task(s) LCDL status
1 Global message search (*, Messages scope, date range, Next, click hit) teams_search_messages_chunk Production
2 Sidebar chat inventory teams_list_chats Production
3 Open chat message pane scroll teams_list_chat_messages_chunk Production

Run 2 live guided ingest: teams_list_chats returned 0 on operator tab — open-chat scroll alone cannot cover a 7-day window across chats. Global message search (teams_search_messages_chunk) is the production cross-chat discovery path.

Global search (production)

LCDL scripts in teams_readonly.py and connectors/teams_search_chunk.py:

Step DOM anchor (Teams v2, observed 2026-06-09) Evaluate op
Expand search [data-tid="title-bar-toggle-search-btn"] click via evaluate
Query input input[data-tid="AUTOSUGGEST_INPUT"] set *, input events
Suggestions popup [data-tid="ms-searchux-popup"] optional; confirm Messages scope
Messages scope Results facet/tab labeled Messages click (not “Find in chat”)
Date filters Search results date from / to controls set to consumer date_from / date_to
Paginate Next / more-results control click until exhausted
Open hit Search result row click → chat opens, message highlighted
Harvest Highlighted row + pane teams_harvest_message_rows or dedicated highlight script

Out of scope for this task: chat-header-find-in-chat-search (per-chat Ctrl+F only).

Cockpit passes date_from / date_to from TeamsHarvestWindow; LCDL does not read Cockpit env for guided ingest.

Governed tasks (v1 + planned)

task_id Module Evaluate scripts Status
teams_probe_health tasks/teams_read_v1.py teams_page_health Production
teams_list_chats tasks/teams_read_v1.py teams_harvest_chat_list Production
teams_list_channels tasks/teams_read_v1.py teams_harvest_channel_list Production
teams_list_chat_messages_chunk tasks/teams_read_v1.py teams_harvest_message_rows, teams_message_scroll_step, teams_restore_scroll Production
teams_list_channel_posts_chunk tasks/teams_read_v1.py same harvest + scroll Production
teams_read_thread_replies tasks/teams_read_v1.py thread navigation + harvest Production
teams_search_messages_chunk production SERP AAMkAD… id, aria-label parse, click+expand, date filter, next page Production

Scripts: src/forge_lcdl/playwright/scripts/teams_readonly.py. Search chunk engine: connectors/teams_search_chunk.py (orchestrates public atoms in connectors/teams_search_atoms.py; post-click ensure_teams_search_surface + day-scope reapply).

Search ingest atoms

Atom Module Responsibility
teams_ensure_search_surface teams_search_atoms Wrap ensure_teams_search_surface
teams_open_messages_search teams_search_atoms Messages tab + * sync
teams_apply_single_day_scope teams_search_atoms Date chip or query scope
teams_verify_single_day_scope teams_search_atoms Chip/query + UI state verdict
teams_guard_single_day_scope teams_search_atoms Per-page re-apply + card cap
teams_harvest_serp_cards teams_search_atoms SERP card harvest + message filter
teams_advance_serp_next_page teams_search_atoms Next pagination
teams_harvest_card_detail teams_search_atoms Click + expand + rail merge
teams_restore_serp_after_detail teams_search_atoms Return to SERP after detail
teams_probe_day_pagination teams_search_atoms Discover-only pagination walk

End-to-end (LCDL slice)

Chat pane chunk (production)

session_ref resolved → PlaywrightPageSession
  → navigate(url) optional (skip_navigate when CDP tab already on chat)
  → evaluate_readonly(teams_page_health)           # probe
  → loop: evaluate_readonly(harvest_message_rows)
         scroll_readonly(message_scroll_step, direction=up|down)
         merge rows → UnifiedItem
         checkpoint (scroll_top, seen_item_ids, at_end)
  → wire JSON: items[], next_cursor, chunk.has_more

Global search chunk (production)

session_ref → PlaywrightPageSession
  → keyboard: Ctrl+Alt+E, Messages tab, ``*`` + Enter
  → evaluate: apply date_from / date_to on search filters (keyboard From/To + Apply)
  → loop: harvest search result rows
         keyboard: PageDown SERP load; focus Next + Enter for more pages
         per hit: focus row + Enter → wait for highlight → harvest_message_rows
         after detail: ensure_teams_search_surface + reapply day scope before next card
         at_end when all cards known and Next disabled
         checkpoint (page_index, seen_item_ids, nav_method)
  → wire JSON: items[], next_cursor, chunk.has_more, warnings[]

Consumer calls via cockpit_server.lcdl_teams_bridge.run_task — LCDL does not import Cockpit.

Responsibility matrix

Concern forge-lcdl Cockpit consumer Edge
Governed read tasks teams_read_v1.py + teams_search_messages_chunk lcdl_teams_bridge.run_task
DOM selectors / evaluate JS teams_readonly.py Must not add page.locator in teams.py Teams v2 UI
Chunk checkpoint wire TeamsChunkCheckpoint in teams_chunk.py Passed as checkpoint / next_cursor in bridge loop
Scroll + virtualization teams_message_scroll_step (direction up/down), restore_scroll scroll_direction, max_scroll_rounds via bridge Message pane visible
Search UI automation teams_search_chunk.py, teams_search_keyboard.py Orchestration, budget, COCKPIT_TEAMS_NAV_MODE (default keyboard), date window filter; SERP virtualization via PageDown in keyboard mode Operator signed in
UnifiedItem mapping teams.py connector + teams_modern.py wire_item_to_cockpit_fields
HTML snapshot parser (legacy) connectors/teams.py _TeamsListParser Fallback only if COCKPIT_TEAMS_DOM_FALLBACK=1
CDP tab pick pick_teams_cdp_page_target, teams_urls_equivalent_for_nav teams_cdp_attach.py + routine sync Existing Teams tab
Tab visibility (background ingest) apply_cdp_focus_emulation on attach Same env as Outlook (FORGE_LCDL_CDP_FOCUS_EMULATION, bring-to-front off) Chat list must not stay document.hidden
unified_message, sync_run connectors/teams.py
Sync UI labels SyncProgressPanel.tsx
Read-only guarantee READONLY_POLICY.md, task read_only: true audit_read_only_ingestion Operator obeys policy

What LCDL must not do

Forbidden in LCDL Belongs in consumer
unified_message, message_channel Cockpit SQLite
Ingest budget / channel cap 100 sync_service.py
write_capture, files/Teams/ paths Cockpit storage.py
Checkpoint files on disk Consumer optional (Teams: in-memory loop today)
Plaud, Graph app registration Cockpit / org MSAL
TeamsHarvestWindow / guided manifest Cockpit teams_harvest_window.py, guided scripts

See MESSAGING-CONSUMER-BOUNDARY.md.

Preview vs thread detail

Task Body depth
teams_search_messages_chunk Search hit preview; fuller text from highlighted pane row when click succeeds
teams_list_chat_messages_chunk body_preview from list row DOM (~500 chars)
teams_read_thread_replies Reply text under opened thread

No task downloads chat attachments or SharePoint files.

Run status / open questions

Topic LCDL status
Upward scroll for older chat history Done (run 2)direction=up, scrollable ancestor picker
Author/body split in harvest rows Done (run 2)
teams_search_messages_chunk Production — Exchange SERP ids + click-through body harvest
@mentions harvest script Deferred
Shared files tab metadata Deferred
In-page REST capture TBD — consumer network sniff only
Channel posts in default sync loop Deferred — task exists, consumer does not loop it yet