toga-ai 1.0.69 → 1.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,3 +7,4 @@
7
7
  | [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
8
8
  | [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
9
9
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
10
+ | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini |
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Teams Meeting Transcript Export
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-12
10
+ owners: ["ajean"]
11
+ files:
12
+ - worker2/Worker/Team/Transcripts.php
13
+ - worker2/Config/production.ini
14
+ related:
15
+ - ../architecture.md
16
+ - ./creating-worker-actions.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for
22
+ Teams meeting transcripts produced by a set of organizers, classifies each meeting by
23
+ client (from the meeting title), and archives the raw WebVTT to S3. Scheduled via three
24
+ `Core.CronJobs` rows (weekdays 10:00 / 13:00 / 17:30 Central).
25
+
26
+ ## Key files / entry points
27
+
28
+ - `Worker/Team/Transcripts.php` — the whole feature (abstract class, static methods).
29
+ - `Config/production.ini` `[teams]` section — Entra app creds, organizer source, S3 target.
30
+ - `[teamsClientAliases]` config section — alias → canonical client name for classification.
31
+ - Ledger table `Team.TranscriptExports` (DB alias `_underscore::DB_TEAM`, core cluster).
32
+
33
+ ## How it works
34
+
35
+ Per run, after acquiring a Graph client-credentials token:
36
+
37
+ 1. **Resolve organizers** via `resolveConfiguredOrganizers()`:
38
+ - **Preferred:** the Entra security group in `[teams] organizer_group_id`. `getGroupMemberIds()`
39
+ calls `GET /groups/{id}/members/microsoft.graph.user?$select=id`, which returns member
40
+ **object-ids (GUIDs)** directly — exactly what `getAllTranscripts` requires. Uses the
41
+ `GroupMember.Read.All` app permission. Admins add/remove organizers in Entra with **no redeploy**.
42
+ - **Fallback:** the comma-separated `[teams] organizer_user_ids` list (used only when no group
43
+ is configured or the group resolves no members; group-lookup failures are caught so they
44
+ can't abort the run).
45
+ 2. Per organizer: `getAllTranscripts(meetingOrganizerUserId='{guid}', startDateTime, endDateTime)`
46
+ over an incremental window (`MAX(dtCreated)` watermark from the ledger, else `now − lookbackDays`),
47
+ following `@odata.nextLink`.
48
+ 3. Per new transcript (dedup by `transcriptIdentifier`): fetch meeting `subject`+`startDateTime`
49
+ (cached per meeting), classify, download VTT (`?$format=text/vtt`), `putObject` to S3, insert
50
+ ledger row.
51
+
52
+ **Classification:** case-insensitive substring match of the meeting title against active
53
+ `Core.Clients` names + `[teamsClientAliases]`, longest needle wins, min length 3. No match → `general`.
54
+
55
+ **S3 layout:** `s3://{[teams] s3_bucket}/{s3_prefix}{client-or-general}/{YYYY-MM-DD}/{HHMM}_{title-slug}_{shortId}.vtt`
56
+ (date/time in Central). Currently `toga-private/transcripts/…`.
57
+
58
+ ## Data model
59
+
60
+ `Team.TranscriptExports` — unique `transcriptIdentifier` (idempotency + per-organizer watermark
61
+ via `MAX(dtCreated)`). Columns: `uuid, dtExported, dtCreated, dtMeeting, meetingSubject,
62
+ sizeBytes, classification, transcriptIdentifier, meetingIdentifier, organizerUserIdentifier, s3Key`.
63
+
64
+ ## Entra app permissions (app-only / client credentials)
65
+
66
+ The dedicated Entra app **Teams-Worker2-TranscriptExport** needs, admin-consented:
67
+
68
+ - `OnlineMeetingTranscript.Read.All` — read transcript content.
69
+ - `OnlineMeetings.Read.All` — read meeting metadata.
70
+ - `GroupMember.Read.All` — read the organizer group's members (the object-ids).
71
+
72
+ `User.Read.All` is **deliberately not used** — the group-members call already returns object-ids,
73
+ so the old per-UPN `/users/{upn}?$select=id` lookup is unnecessary.
74
+
75
+ No Teams **Application Access Policy** is required for this tenant — verified the app can read
76
+ the organizers' transcripts directly once given a valid object-id.
77
+
78
+ ## Client variations
79
+
80
+ None — uniform. Output is partitioned per client by meeting-title classification, but the
81
+ process is identical for all.
82
+
83
+ ## Gotchas / known issues
84
+
85
+ - **`getAllTranscripts` requires an AAD object-id (GUID), NOT a UPN.** Passing a UPN
86
+ (e.g. `ajean@togatech.com`) returns the opaque `HTTP 400 BadRequest / "UnknownError"`.
87
+ This is the single most likely cause of an export failure.
88
+ - **The earlier UPN→GUID fix (PR #77) silently failed.** It resolved each UPN via
89
+ `GET /users/{upn}?$select=id`, but the app lacks `User.Read.All`, so that call returns
90
+ `403 Authorization_RequestDenied`, which `resolveUserId()` swallows and falls back to the UPN —
91
+ producing the same 400. Lesson: a "fix" that depends on an ungranted permission and silently
92
+ falls back will look correct in code review but never work in prod. The group-members approach
93
+ avoids the `/users` endpoint entirely.
94
+ - `resolveUserId()` still exists for the `organizer_user_ids` fallback path and returns a GUID
95
+ unchanged (regex match); it is bypassed entirely when the group is configured.
96
+ - Graph meeting/transcript ids are base64 and contain `+ / =` — they must be `rawurlencode()`'d
97
+ at every Graph call site (`getMeeting`, `downloadTranscriptVtt` fallback URL).
98
+ - `resolveWatermark()` guards against an unparseable stored `dtCreated`: without the guard,
99
+ `strtotime(...) === false` minus 60 yields a 1969 date and a decades-wide Graph query window.
100
+ - Plaintext `client_secret` lives in `Config/production.ini` `[teams]` — should be rotated /
101
+ moved to a secret store (out of scope as of this writing).
102
+
103
+ ## Diagnosing a 400
104
+
105
+ A standalone bash diagnostic (`worker2/diagnose-transcripts.sh`) reproduces the Graph call
106
+ chain step-by-step (token → group members → `getAllTranscripts`) and prints the **full** Graph
107
+ error body that the worker discards (it keeps only `error.message`). It reads creds from the ini
108
+ at runtime and prints none. Useful first stop for any future Graph failure here.
109
+
110
+ ## Change history
111
+
112
+ - 2026-06-12 — Resolve organizers from the `organizer_group_id` Entra group (object-ids via
113
+ GroupMember.Read.All) instead of per-UPN lookup; fixes the recurring HTTP 400 that PR #77's
114
+ `/users` approach couldn't (no User.Read.All). Hardened watermark parsing, URL-encoding, and
115
+ silent catches. Verified live: 6/6 organizers, 32 transcripts. (ajean)
116
+
117
+ ## Related docs
118
+
119
+ - [Worker (worker2) Architecture](../architecture.md)
120
+ - [Creating Worker Actions](./creating-worker-actions.md)
@@ -12,7 +12,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
12
12
  ## 2.0 framework
13
13
 
14
14
  - **_underscore** (_Underscore) _(framework core)_ — 5 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
15
- - **worker2** (Worker) — 5 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
15
+ - **worker2** (Worker) — 6 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
16
16
  - **api2** (API) — 1 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
17
17
  - **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
18
18
  - **toga2-supply** (TOGa Supply) — 2 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.69",
3
+ "version": "1.0.70",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",