toga-ai 1.0.301 → 1.0.303
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.
- package/knowledge/1.0/apps/togadesk/INDEX.md +1 -0
- package/knowledge/1.0/apps/togadesk/features/managed-service-order-create.md +109 -0
- package/knowledge/1.0/apps/tools/features/talos-kb-documents-admin.md +15 -2
- package/knowledge/1.0/standards/backend-php.md +22 -2
- package/knowledge/2.0/apps/_underscore/features/async-query-execution.md +22 -1
- package/knowledge/2.0/apps/worker2/INDEX.md +3 -3
- package/knowledge/2.0/apps/worker2/architecture.md +22 -1
- package/knowledge/2.0/apps/worker2/features/notification-email-template.md +26 -4
- package/knowledge/2.0/apps/worker2/features/talos-transcript-ingestion.md +153 -154
- package/knowledge/2.0/apps/worker2/features/teams-transcript-export.md +18 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| [TOGa Desk Architecture](architecture.md) | TOGa Desk is the staff-facing support desk **and field-service platform** (analysts work at `/desk/`). | desk/index.php, desk/includes/loader.php, desk/config.php, desk/includes/controllers/actions.php, desk/includes/controllers/data.php, desk/includes/controllers/modals.php, desk/includes/classes/class.app.php, desk/includes/classes/class.ticket.php, desk/api/index.php, desk/api/resources/tickets.php, crons/tickets.php |
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk ingests support email into tickets through a cron-driven IMAP poller (`crons/tickets.php`) plus a postfix pipe variant (`crons/pipe.php`). | crons/tickets.php, crons/tickets_prod.php, crons/pipe.php, desk/includes/classes/class.ticket.php |
|
|
7
7
|
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | The **central** subsystem is TOGa Desk's field-service dispatch domain: repair-order lifecycle, technician scheduling, onsite vs depot service, parts, and shipm | desk/includes/controllers/actions/central/, desk/includes/classes/class.repair.php, desk/includes/classes/class.repairhistory.php, desk/_/browser/datatable/central.php, desk/template/pages/central.php, desk/template/pages/central/view.php, desk/template/modals/central/addTracking.php, library/app/model/togadesk/repairorder.php, library/app/model/togadesk/repairordertracking.php, library/app/api/carrier/ups.php |
|
|
8
|
+
| [Managed Service Order Create Flow (New MSO modal → tickets/add)](features/managed-service-order-create.md) | The **Managed Service Order (MSO) create flow** is how a TOGa Desk user manually creates a managed service order: pick an End User (or a Client Location), choos | desk/includes/controllers/actions/tickets/add.php, desk/template/modals/tickets/addNew.php, desk/template/pages/managed.php, desk/template/pages/getinfo.php, desk/template/footer.php, library/app/model/togadesk/repairorder.php |
|
|
8
9
|
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, crons/tickets.php, crons/tickets_prod.php |
|
|
9
10
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk exposes a programmatic API at `desk/api/`. | desk/api/index.php, desk/api/resources/tickets.php, desk/api/resources/assets.php, desk/api/resources/authenticate.php, desk/includes/classes/class.apikey.php, desk/includes/functions.php |
|
|
10
11
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | The SMB contracts page (`/desk/?route=toga/smbcontracts&togaClientId=<id>`) edits `TOGA_*.SMBContracts` rows via a modal. | desk/template/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/edit.php |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Managed Service Order Create Flow (New MSO modal → tickets/add)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: togadesk
|
|
5
|
+
project: TOGa Desk
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- desk/includes/controllers/actions/tickets/add.php
|
|
13
|
+
- desk/template/modals/tickets/addNew.php
|
|
14
|
+
- desk/template/pages/managed.php
|
|
15
|
+
- desk/template/pages/getinfo.php
|
|
16
|
+
- desk/template/footer.php
|
|
17
|
+
- library/app/model/togadesk/repairorder.php
|
|
18
|
+
related:
|
|
19
|
+
- 1.0/apps/togadesk/architecture.md
|
|
20
|
+
- 1.0/apps/togadesk/features/field-service-dispatch.md
|
|
21
|
+
- 1.0/apps/togadesk/features/ticket-lifecycle.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
The **Managed Service Order (MSO) create flow** is how a TOGa Desk user manually creates a
|
|
26
|
+
managed service order: pick an End User (or a Client Location), choose a Contract, add one or
|
|
27
|
+
more assets, and submit. The server builds a `ManagedServiceOrder`, creates its assets, then
|
|
28
|
+
one `App_Model_TogaDesk_RepairOrder` per asset. This is the **entry/creation** side; the
|
|
29
|
+
downstream repair-order lifecycle (dispatch, scheduling, status) lives in
|
|
30
|
+
[Field-Service Dispatch](./field-service-dispatch.md).
|
|
31
|
+
|
|
32
|
+
## Key files / entry points
|
|
33
|
+
- **Entry link:** `desk/template/pages/managed.php` — the "NEW MANAGED SERVICE ORDER" link opens
|
|
34
|
+
modal `tickets/addNew`.
|
|
35
|
+
- **Modal:** `desk/template/modals/tickets/addNew.php` — the New MSO form. It has **no own
|
|
36
|
+
`<form>` tag**; it relies on the global `#formModal` wrapper (rendered in
|
|
37
|
+
`desk/template/footer.php`) and `$("form").submit()`. Contains the Summary field, an optional
|
|
38
|
+
inline **new End User** sub-form (name/email/phone + a `#new-user-password` field), an inline
|
|
39
|
+
**new address** sub-form, and select2 dropdowns `#enduser`, `#userlocation`, `#location`.
|
|
40
|
+
- **Server action:** `desk/includes/controllers/actions/tickets/add.php` — builds the
|
|
41
|
+
`ManagedServiceOrder`, creates assets, then a `RepairOrder` per asset. `ticketType`
|
|
42
|
+
`INSTALLATION` maps the repair order to `App_Model_TogaDesk_RepairOrder::TYPE_ONSITESERVICE`.
|
|
43
|
+
- **Address AJAX:** `desk/template/pages/getinfo.php` — populates the End User Address dropdown.
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
1. `managed.php` opens the `tickets/addNew` modal into the global `#formModal`.
|
|
47
|
+
2. The user selects an existing End User (`#enduser` → `userid`) or creates a new one inline;
|
|
48
|
+
selects an End User Address (`#userlocation`) or a Client Location (`#location`); picks a
|
|
49
|
+
Contract; and adds assets.
|
|
50
|
+
3. Submit runs the modal's client-side validator, then POSTs `?action=…` to `add.php`.
|
|
51
|
+
4. `add.php` builds the `ManagedServiceOrder`, creates each asset, and for each asset builds a
|
|
52
|
+
`RepairOrder`. In the `TYPE_ONSITESERVICE` branch it reads optional per-asset fields
|
|
53
|
+
(`installation`, `access-instructions`) — see the getVar gotcha below.
|
|
54
|
+
|
|
55
|
+
### Address sourcing — per-person vs. client location (important)
|
|
56
|
+
There are **two** address sources, and they come from different tables:
|
|
57
|
+
- **End User Address dropdown (`#userlocation`)** is per **person**. `getinfo.php` (uid branch,
|
|
58
|
+
~L152–171) runs `SELECT … FROM people_addresses INNER JOIN addresses ON addresses.id =
|
|
59
|
+
people_addresses.addressid WHERE people_addresses.peopleid = <uid>`. Addresses are stored
|
|
60
|
+
**per person** in the `addresses` table + `people_addresses` junction (`peopleid` → `addressid`)
|
|
61
|
+
in the togadesk DB (`TOGaDeskSupport` / `db_togadesk`).
|
|
62
|
+
- **An empty End User Address dropdown is a legitimate data state**, not a bug — it just means
|
|
63
|
+
that person has no `people_addresses` row. The **designed fallback** is the modal's inline
|
|
64
|
+
"add new address" form (`addNewAddress()` sets the `#newAddressCount` hidden field), which
|
|
65
|
+
`add.php` (~L119–148) fully supports creating.
|
|
66
|
+
- **Client Location (`#location`)** is the alternative: client/site addresses live in the
|
|
67
|
+
`locations` table keyed by `clientid`, reachable via the modal's "Select either End User or
|
|
68
|
+
Client Location" toggle (`#checkbox` → `#servicelocation`/`#location`), enabled once a Contract
|
|
69
|
+
is chosen.
|
|
70
|
+
|
|
71
|
+
History: the per-person-only End User query dates to a **2021-12-06** schema migration
|
|
72
|
+
(`repair_order_addresses`/`primaryaddress` → `addresses`/`isprimary`, commit `bf1abaa06`, Jeff).
|
|
73
|
+
The older query — still commented in `getinfo.php` ~L95–148 — UNIONed client `locations` into the
|
|
74
|
+
End User dropdown. Dropping that UNION is **long-standing, not a recent regression**.
|
|
75
|
+
|
|
76
|
+
## Gotchas / known issues
|
|
77
|
+
- **`App_Page::getVar($var)` THROWS when the request var is absent** — it is **not** a
|
|
78
|
+
null-returning getter. In the `TYPE_ONSITESERVICE` branch, `add.php` (~L353–354) read the
|
|
79
|
+
optional `installation` and `access-instructions` fields; the manual MSO form doesn't post
|
|
80
|
+
`installation`, so the whole create aborted with a Sentry exception. **Fix:** use
|
|
81
|
+
`App_Page::getVarIfSet($var)` (returns `null`) — or guard with `App_Page::varIsSet()` — for any
|
|
82
|
+
**optional** request field. This is a framework-wide 1.0 `App_` gotcha, not togadesk-specific
|
|
83
|
+
(`library/app/page.php` — `getVar` L354 throws, `getVarIfSet` L382 returns null,
|
|
84
|
+
`varIsSet` L404).
|
|
85
|
+
- **Chrome autofill silently breaks the modal.** The modal nests a new-user sub-form (including a
|
|
86
|
+
password field) inside `#formModal`, which had no `autocomplete` attribute. Chrome's
|
|
87
|
+
password/profile autofill then filled Summary + address fields **without firing jQuery
|
|
88
|
+
change/input events and without syncing the select2 widgets**, so validation saw empty values
|
|
89
|
+
(e.g. "Please select an address" on a field that looked filled). Manual typing worked because it
|
|
90
|
+
fires the events. **Fix:** `autocomplete="off"` on `#formModal` (and the new-user/new-address/
|
|
91
|
+
Summary/select2 fields) plus `autocomplete="new-password"` on the password field (Chrome ignores
|
|
92
|
+
form-level `off` for password fields).
|
|
93
|
+
- **No-saved-address submit deadlock.** When an existing End User (`userid != 0`) had no saved
|
|
94
|
+
address, the validator required `#userlocation != 0` ("Please select an address."), but the
|
|
95
|
+
dropdown was empty and the modal auto-opened the inline add-address form — an unresolvable
|
|
96
|
+
deadlock. **Fix:** the validator now also reads `newAddressCount = parseInt($('#newAddressCount')
|
|
97
|
+
.val())||0` and only blocks when `selectedValue == 0 AND newAddressCount == 0`, since the server
|
|
98
|
+
already supports the inline address.
|
|
99
|
+
- The addNew modal has **no `<form>` tag of its own** — it depends entirely on the global
|
|
100
|
+
`#formModal` in `footer.php` and `$("form").submit()`. Anything that alters that global wrapper
|
|
101
|
+
(attributes, nesting) affects this flow.
|
|
102
|
+
|
|
103
|
+
## Change history
|
|
104
|
+
- 2026-07-09 — TRUE-80100: documented the MSO create flow (entry, server action, per-person vs.
|
|
105
|
+
client-location address sourcing) and folded in three fixes: `getVar` → `getVarIfSet` for
|
|
106
|
+
optional request vars in the `TYPE_ONSITESERVICE` branch (server crash); `autocomplete` on
|
|
107
|
+
`#formModal` + password field to stop Chrome autofill bypassing select2/events; and the
|
|
108
|
+
validator now honors `#newAddressCount` so a no-address End User can submit an inline address.
|
|
109
|
+
(mhammontree)
|
|
@@ -6,7 +6,7 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/mvc/talos/kb-documents/get.php
|
|
@@ -68,11 +68,18 @@ Classic view → Edit → Save per tab, with **full-set reconciliation** (transa
|
|
|
68
68
|
back on error), trash-icon row removal, and an auto-dismissing toast. Backs the `Team.*`
|
|
69
69
|
prompt/replacement tables that the worker2 pipeline reads at runtime.
|
|
70
70
|
|
|
71
|
+
**Encoding (fixed 2026-07-09):** terms were stored HTML-encoded and then re-escaped at render,
|
|
72
|
+
so they displayed **double-encoded** (e.g. "Grand & Toy"). `vocabulary/post.php` now
|
|
73
|
+
`html_entity_decode()`s values on save so the DB stores **raw** text; `vocabulary/get.php`
|
|
74
|
+
already escapes once at display. Do not re-add encoding on save.
|
|
75
|
+
|
|
71
76
|
## Helpers
|
|
72
77
|
|
|
73
78
|
- **`App_Talos_S3`** (`_/app/talos/s3.php`) — S3 client; list KB slugs; list/count approved
|
|
74
79
|
(UTC→Central for display); presigned URL; get/delete; `moveApprovedDocument`;
|
|
75
|
-
`updateApprovedDocument`; `sanitizeTitle
|
|
80
|
+
`updateApprovedDocument`; `sanitizeTitle` (**now converts `/` and `\` to hyphens** — it
|
|
81
|
+
previously converted them to spaces, which let a slashed title create an unintended S3
|
|
82
|
+
sub-folder; matches worker2's `filenameSafeTitle()`); `copyWithinBucket`; `putObjectBody`; slug
|
|
76
83
|
sanitize+lowercase; approved-prefix path guard. **`client()` credential resolution:** prefers a
|
|
77
84
|
dedicated **`[talos]`** config section (the **togaiq**-capable key), falling back to `[aws]`, then
|
|
78
85
|
the SDK default chain — because `App_Talos_S3` only ever talks to **togaiq**, so it needs the
|
|
@@ -114,6 +121,12 @@ prompt/replacement tables that the worker2 pipeline reads at runtime.
|
|
|
114
121
|
|
|
115
122
|
## Change history
|
|
116
123
|
|
|
124
|
+
- 2026-07-09 — Fixed `App_Talos_S3::sanitizeTitle()` to convert `/`,`\` to hyphens (was
|
|
125
|
+
spaces) so a slashed meeting title no longer creates a stray S3 sub-folder — matches
|
|
126
|
+
worker2's `filenameSafeTitle()`; `updateApprovedDocument()` already renames the S3 object and
|
|
127
|
+
updates the metadata-sidecar filename on a title edit. Fixed the vocabulary UI double-encoding
|
|
128
|
+
bug: `vocabulary/post.php` now `html_entity_decode()`s on save so the DB stores raw text
|
|
129
|
+
(`get.php` escapes once at display). (jcardinal)
|
|
117
130
|
- 2026-07-02 — Added **move** (re-file a doc between KBs) and **edit** (title + Find&Replace + body,
|
|
118
131
|
ported from the old `kb_processor.php`) to the doc modal, each via get+put (togaiq denies
|
|
119
132
|
`CopyObject`) and each enqueuing a single `SyncKb("source,dest")` re-sync. Fixed togaiq
|
|
@@ -5,8 +5,8 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-07-
|
|
9
|
-
owners: [jcardinal, rgirish]
|
|
8
|
+
updated: 2026-07-09
|
|
9
|
+
owners: [jcardinal, rgirish, mhammontree]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/library/architecture.md
|
|
@@ -429,6 +429,26 @@ gracefully instead of dropping unrelated work or crashing the loop.
|
|
|
429
429
|
|
|
430
430
|
* Validate and sanitize all user input (see SQL Injection Prevention and HTML escaping above).
|
|
431
431
|
|
|
432
|
+
### Request variable access — `getVar` throws, `getVarIfSet` returns null
|
|
433
|
+
|
|
434
|
+
`App_Page::getVar($var)` is **not** a null-returning getter — it **throws**
|
|
435
|
+
`Exception("Variable '$var' does not exist in the request headers.")` when the request var is
|
|
436
|
+
absent (`library/app/page.php` L354). Use it only for fields you *know* are always posted.
|
|
437
|
+
|
|
438
|
+
For any **optional** request field, use `App_Page::getVarIfSet($var)` (returns `null` when
|
|
439
|
+
missing, L382) or guard first with `App_Page::varIsSet($var)` (L404). Reading an optional field
|
|
440
|
+
with `getVar()` aborts the whole request with an uncaught exception — this is a recurring 1.0
|
|
441
|
+
crash class (e.g. TRUE-80100: an unconditional `getVar('installation')` in a create action
|
|
442
|
+
crashed every submit that omitted the field).
|
|
443
|
+
|
|
444
|
+
```php
|
|
445
|
+
// WRONG — throws when 'installation' isn't posted
|
|
446
|
+
$order->location = App_Page::getVar('installation');
|
|
447
|
+
|
|
448
|
+
// CORRECT — optional field
|
|
449
|
+
$order->location = App_Page::getVarIfSet('installation');
|
|
450
|
+
```
|
|
451
|
+
|
|
432
452
|
### Secure Communication
|
|
433
453
|
|
|
434
454
|
* Always use HTTPS to encrypt data in transit.
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Query.php
|
|
@@ -74,6 +74,23 @@ and is a harmless no-op.
|
|
|
74
74
|
connection and started a local transaction it never committed. Fixed by moving the async
|
|
75
75
|
dispatch to the **top** of `execute()`, before any read/write connection or transaction setup.
|
|
76
76
|
|
|
77
|
+
## When to use async vs. synchronous (usage rule)
|
|
78
|
+
|
|
79
|
+
Async is a **workaround for a stale connection after a long HTTP call**, not a
|
|
80
|
+
general-purpose "make writes faster" switch. Use it **only** for writes that happen *after* a
|
|
81
|
+
long-running (e.g. up to 600s) external call has left the local connection idle enough to hit
|
|
82
|
+
"MySQL server has gone away" — for example the **post-AI `setStatus` writes** in the Talos
|
|
83
|
+
transcript pipeline.
|
|
84
|
+
|
|
85
|
+
**Do NOT use async for a write another poll/step must read back.** An async write is not
|
|
86
|
+
durable on the caller's connection when control returns — it commits later, on the worker's
|
|
87
|
+
connection. In the Talos pipeline the **pre-AI dedupe / discovery-ledger writes**
|
|
88
|
+
(`recordExport`, `loadOrCreateProcessing` INSERT) were originally async and the rows weren't
|
|
89
|
+
durable before the next poll, so dedupe and the watermark failed and meetings reprocessed.
|
|
90
|
+
Those writes were changed to **synchronous + `_Database::transactionCommit(...)`**. Rule of
|
|
91
|
+
thumb: **read-back-before-worker-runs → synchronous+committed; fire-and-forget-after-slow-call
|
|
92
|
+
→ async.** A stale read after such a write may also need `_Database::useQueryCache(false)`.
|
|
93
|
+
|
|
77
94
|
## Known open items
|
|
78
95
|
|
|
79
96
|
- **SECURITY (open, developer aware).** DB username/password are currently sent in **plaintext**
|
|
@@ -83,6 +100,10 @@ and is a harmless no-op.
|
|
|
83
100
|
|
|
84
101
|
## Change history
|
|
85
102
|
|
|
103
|
+
- 2026-07-09 — Added the sync-vs-async usage rule (async is only for fire-and-forget writes
|
|
104
|
+
after a long external call that staled the connection; writes another step must read back
|
|
105
|
+
must stay synchronous+committed). Prompted by the Talos pipeline, where async pre-AI dedupe
|
|
106
|
+
writes weren't durable before the next poll and caused reprocessing. (jcardinal)
|
|
86
107
|
- 2026-07-07 — Built writes-only async query execution (`_Query` `isAsync` → Worker
|
|
87
108
|
`Infrastructure/Database/Query` action); fixed four correctness bugs found in review
|
|
88
109
|
(constructor order, worker-side commit, double-dispatch sentinel, dangling local
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
| [NetSuite ↔ ClickUp / TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Clickup.php, worker2/Worker/Clickup/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
19
19
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
20
20
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
21
|
-
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, _underscore/Model/Client/EmailTemplate.php |
|
|
21
|
+
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
22
22
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
23
23
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
24
24
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
25
25
|
| [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
|
|
26
|
-
| [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts`
|
|
26
|
+
| [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` runs a fully automated, cron-driven pipeline that ingests raw Teams transcripts into the **Talos / TOGa IQ** AWS Bedrock knowledge ba | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a |
|
|
27
27
|
| [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
|
|
28
|
-
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) |
|
|
28
|
+
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
|
|
29
29
|
| [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
|
|
30
30
|
| [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
|
|
31
31
|
| [Ticket → ClickUp Pseudocode Planning (Talos-grounded)](workflows/ticket-to-pseudocode-planning.md) | A repeatable procedure for turning a ClickUp ticket into a reviewed, formatted implementation plan posted back to the ticket's `📝 Pseudocode` custom field. | test/@dave/clickup_md2delta.js |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Controller/Index.php
|
|
@@ -154,6 +154,23 @@ worker tier creates the row itself on pickup.) Reason: the caller's MySQL transa
|
|
|
154
154
|
open when SQS delivers; the worker reads in a separate connection and, if uncommitted, its
|
|
155
155
|
SELECT returns nothing and the guard fires ("already processed or not found — skipping").
|
|
156
156
|
|
|
157
|
+
## Long-running AI actions — sync vs. async writes
|
|
158
|
+
|
|
159
|
+
A worker action that makes a long (up to 600s) AI/HTTP call leaves its **local MySQL
|
|
160
|
+
connection idle** long enough to hit "MySQL server has gone away" on the *next* write. The
|
|
161
|
+
pattern that works (proven in the Talos transcript pipeline):
|
|
162
|
+
|
|
163
|
+
- **Writes that a later poll/step must read back go BEFORE the AI call, synchronous +
|
|
164
|
+
committed.** e.g. the transcript dedupe/discovery-ledger inserts. If these are dispatched
|
|
165
|
+
async (via `_Query` writes-only async → `Infrastructure/Database/Query`) the row is **not
|
|
166
|
+
durable** when control returns, so the next cron poll re-selects and reprocesses the same
|
|
167
|
+
item. A stale read immediately after may also need `_Database::useQueryCache(false)`.
|
|
168
|
+
- **Writes AFTER the AI call (e.g. `setStatus`) go async** — the connection is already stale,
|
|
169
|
+
and these are fire-and-forget status updates nobody reads back synchronously.
|
|
170
|
+
|
|
171
|
+
See [async query execution](../_underscore/features/async-query-execution.md) for the `_Query`
|
|
172
|
+
`isAsync` mechanism and its correctness gotchas.
|
|
173
|
+
|
|
157
174
|
## Key design decisions
|
|
158
175
|
|
|
159
176
|
MySQL-first for the **Lambda/webhook/cron ingestion paths** (SQS on success) prevents lost
|
|
@@ -194,6 +211,10 @@ the MySQL-first design was departed from **on purpose** here.
|
|
|
194
211
|
|
|
195
212
|
## Change history
|
|
196
213
|
|
|
214
|
+
- 2026-07-09 — Added the "long-running AI actions — sync vs. async writes" rule: reads-back
|
|
215
|
+
writes go before the AI call, synchronous+committed; post-AI status writes go async.
|
|
216
|
+
Distilled from the Talos Graph-direct pipeline (async pre-AI dedupe writes weren't durable
|
|
217
|
+
before the next poll → reprocessing). (jcardinal)
|
|
197
218
|
- 2026-06-08 — Initial worker2 architecture doc. (jcardinal)
|
|
198
219
|
- 2026-07-07 — `_Worker::runTask()` moved to SQS-first (caller does no MySQL; worker tier
|
|
199
220
|
creates/tracks the row on pickup); the former debug-only direct-payload path in
|
|
@@ -6,10 +6,11 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["mhammontree"]
|
|
9
|
+
updated: 2026-07-09
|
|
10
|
+
owners: ["mhammontree", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Notification/EmailTemplate.php
|
|
13
|
+
- worker2/Worker/Client/True.php
|
|
13
14
|
- _underscore/Model/Client/EmailTemplate.php
|
|
14
15
|
related:
|
|
15
16
|
- ./notification-email.md
|
|
@@ -36,7 +37,8 @@ silently riding inside — the originating transaction.
|
|
|
36
37
|
|
|
37
38
|
- `worker2/Worker/Notification/EmailTemplate.php` —
|
|
38
39
|
`abstract class _Worker_Notification_EmailTemplate`. One method:
|
|
39
|
-
`public static Send(string $clientIdentifier, string $uuid, $to, $cc, $bcc, ...$args): string`.
|
|
40
|
+
`public static Send(string $clientIdentifier, string $uuid, string|array $to = [], string|array $cc = [], string|array $bcc = [], ...$args): string`.
|
|
41
|
+
`to`/`cc`/`bcc` accept a string or array and **default to `[]`** when omitted.
|
|
40
42
|
- Resolves the numeric `clientId` from `$clientIdentifier` via a Core query.
|
|
41
43
|
- Registers the client DB with
|
|
42
44
|
`_Database::registerClientDatabases($clientId, $environment)` (see the DB-registration
|
|
@@ -61,7 +63,21 @@ silently riding inside — the originating transaction.
|
|
|
61
63
|
pattern: `_Worker_Startech`). See
|
|
62
64
|
[`creating-worker-actions.md`](./creating-worker-actions.md).
|
|
63
65
|
3. **Dispatch the template.** `_Model_Client_EmailTemplate::send()` loads the `EmailTemplates`
|
|
64
|
-
row by UUID, substitutes `{placeholder}` vars from `...$args
|
|
66
|
+
row by UUID, substitutes `{placeholder}` vars from `...$args` into **both the stored subject
|
|
67
|
+
and body** (the body may contain HTML), and sends via `_Email`.
|
|
68
|
+
4. **Failure handling.** A failed send **THROWS** and is recorded as a failed `WorkerJobs` row
|
|
69
|
+
(`isSuccess=0`) with **no retry**; the failure never affects whatever enqueued it (the send
|
|
70
|
+
is fully decoupled from the originating transaction).
|
|
71
|
+
|
|
72
|
+
### TOGA Technology (`True`) template
|
|
73
|
+
|
|
74
|
+
The TOGA Technology template constant lives in worker2:
|
|
75
|
+
`_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY = '232d4edb-c2fa-4a8b-b5b9-d5800c962e19'`
|
|
76
|
+
(`worker2/Worker/Client/True.php`); its `clientIdentifier` is `'True'`. The stored template
|
|
77
|
+
must define `{subject}` and `{body}` placeholders (the body may be HTML). This is the template
|
|
78
|
+
the **Talos transcript recap email** uses — see
|
|
79
|
+
[Talos Transcript Ingestion](./talos-transcript-ingestion.md). (The UUID is a template
|
|
80
|
+
identifier, not a secret.)
|
|
65
81
|
|
|
66
82
|
### Enqueuing it
|
|
67
83
|
|
|
@@ -86,6 +102,12 @@ flow into `...$args` as the template variables.
|
|
|
86
102
|
|
|
87
103
|
## Change history
|
|
88
104
|
|
|
105
|
+
- 2026-07-09 — Documented the full `Send` signature (`to`/`cc`/`bcc` default `[]`; extra
|
|
106
|
+
string-keyed params spread as named args into `...$args` → `{placeholder}` subs in subject
|
|
107
|
+
and body, body may be HTML) and the failure contract (throws → failed `WorkerJobs` row,
|
|
108
|
+
`isSuccess=0`, no retry, never affects the enqueuer). Added the TOGA Technology (`True`)
|
|
109
|
+
template constant `_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY` — the template
|
|
110
|
+
the Talos recap email sends through. (jcardinal)
|
|
89
111
|
- 2026-06-30 — Built the generic background email-template worker
|
|
90
112
|
`_Worker_Notification_EmailTemplate::Send` (TRUE-79251): resolves the client, registers the
|
|
91
113
|
client DB via `registerClientDatabases`, and dispatches a stored `EmailTemplates` row by
|
|
@@ -6,11 +6,10 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Team/Transcripts.php
|
|
13
|
-
- worker2/bin/reprocess-transcripts.php
|
|
14
13
|
- worker2/bin/sync-knowledge-bases.php
|
|
15
14
|
- worker2/Config/production.ini
|
|
16
15
|
- dbchanges2/Team/2026-06-30a
|
|
@@ -21,197 +20,197 @@ files:
|
|
|
21
20
|
- dbchanges2/Core/2026-06-30a
|
|
22
21
|
- dbchanges2/Core/2026-07-02a
|
|
23
22
|
- dbchanges2/Team/2026-07-02a
|
|
23
|
+
- dbchanges2/Team/2026-07-08a
|
|
24
|
+
- dbchanges2/Team/2026-07-09a
|
|
24
25
|
related:
|
|
25
26
|
- ./teams-transcript-export.md
|
|
27
|
+
- ./notification-email-template.md
|
|
26
28
|
- ./creating-worker-actions.md
|
|
27
29
|
- ../architecture.md
|
|
30
|
+
- ../../_underscore/features/async-query-execution.md
|
|
28
31
|
- ../../../1.0/apps/tools/features/talos-kb-documents-admin.md
|
|
29
32
|
- ../../../1.0/apps/test/features/talos-kb-pipeline.md
|
|
30
33
|
---
|
|
31
34
|
|
|
32
35
|
## Summary
|
|
33
36
|
|
|
34
|
-
`_Worker_Team_Transcripts`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
`_Worker_Team_Transcripts` runs a fully automated, cron-driven pipeline that ingests raw
|
|
38
|
+
Teams transcripts into the **Talos / TOGa IQ** AWS Bedrock knowledge bases and emails a
|
|
39
|
+
recap to the meeting organizer. **As of 2026-07-09 the pipeline is GRAPH-DIRECT**: there is
|
|
40
|
+
**no S3 staging bucket** in the loop anymore. `Export()` is a thin cron poller that pulls
|
|
41
|
+
transcript references from Microsoft Graph and enqueues one `Process` job each; `Process()`
|
|
42
|
+
downloads the VTT straight from Graph and does everything in-memory. The old
|
|
43
|
+
`s3://toga-private/transcripts/` staging bucket is **no longer used at all**, the `Scan()`
|
|
44
|
+
action was **removed**, and `bin/reprocess-transcripts.php` was **deleted** (the `Export`
|
|
45
|
+
cron replaces it).
|
|
46
|
+
|
|
47
|
+
Flow: `Export` (cron) polls Graph per configured organizer → records a discovery-ledger row
|
|
48
|
+
in `Team.TranscriptExports` → enqueues one `Process` job per transcript → `Process` downloads
|
|
49
|
+
the VTT from Graph, strips+groups it, AI clean+classify (pass 1), applies DB replacements,
|
|
50
|
+
uploads approved `.txt` + metadata sidecar to `togaiq`, archives the raw VTT to `togaiq` from
|
|
51
|
+
the in-memory copy → Bedrock `startIngestionJob` → a **second** AI pass returns structured
|
|
52
|
+
JSON that PHP renders to an inline-styled HTML recap email, enqueued to the organizer.
|
|
47
53
|
|
|
48
54
|
## Key files / entry points
|
|
49
55
|
|
|
50
|
-
- `Worker/Team/Transcripts.php` — actions `
|
|
51
|
-
`
|
|
52
|
-
(all alongside the existing `Export`).
|
|
53
|
-
- `bin/reprocess-transcripts.php` — standalone CLI backlog reprocessor.
|
|
56
|
+
- `Worker/Team/Transcripts.php` — actions `Export`, `Process`, `SyncKb`,
|
|
57
|
+
`SyncKnowledgeBases`. **`Scan` was removed.**
|
|
54
58
|
- `bin/sync-knowledge-bases.php` — CLI entry for the Bedrock→`Team.KnowledgeBases` registry
|
|
55
59
|
sync (`--dry-run` supported).
|
|
56
60
|
- `Config/production.ini` `[talos]` and `[teams]` sections — S3/Bedrock targets + the two
|
|
57
|
-
per-account S3 credential sets (see Configuration).
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
per-account S3 credential sets (see Configuration). Note the **toga-private raw read path
|
|
62
|
+
is gone**; the `[teams]` S3 key is no longer used by the ingestion loop.
|
|
63
|
+
- DB config + ledger tables in the `Team.*` schema (see Data model).
|
|
60
64
|
|
|
61
65
|
## How it works
|
|
62
66
|
|
|
63
|
-
### `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
each
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(`
|
|
67
|
+
### `Export(int $lookbackDays, ?int $limit = null, array $cc = [], array $bcc = [])` (cron)
|
|
68
|
+
Thin poller. The old `organizers` parameter was **REMOVED** — organizers are always resolved
|
|
69
|
+
from config now.
|
|
70
|
+
1. For each configured organizer, poll Microsoft Graph for transcripts in the lookback window.
|
|
71
|
+
2. Resolve the organizer email (see *Organizer email resolution*).
|
|
72
|
+
3. Record a discovery-ledger row in `Team.TranscriptExports` — **synchronously, then
|
|
73
|
+
committed** (see the durability gotcha).
|
|
74
|
+
4. Enqueue one `Process` job per transcript.
|
|
75
|
+
|
|
76
|
+
- `$limit` caps enqueues per run (mainly for testing; e.g. `1` = a single meeting).
|
|
77
|
+
- `cc` / `bcc` thread through each `Process` job into the recap email; validated in
|
|
78
|
+
`enqueueRecapEmail` (deduped, organizer excluded). **Fallback:** if the organizer email
|
|
79
|
+
can't be resolved but `cc` is present, the recap sends **TO** the cc addresses.
|
|
80
|
+
- A Graph `callTranscript` already recorded (`alreadyExported()`) is skipped. Export treats a
|
|
81
|
+
Duplicate-entry exception as **"skipped"**, not a hard error.
|
|
82
|
+
|
|
83
|
+
### `Process(int $transcriptId, string $graphUserId, string $meetingId, string $organizerEmail, string $subject, string $meetingStartIso, array $cc = [], array $bcc = [])`
|
|
84
|
+
1. Download the VTT **straight from Graph** (no S3 round trip).
|
|
85
|
+
2. `stripAndGroupVtt()` — strips WebVTT timestamps and **merges consecutive same-speaker cues
|
|
86
|
+
into one block** (~40% token reduction before the AI call).
|
|
87
|
+
3. **AI clean + classify (pass 1)** — same single-best-KB-or-`general` contract as before
|
|
88
|
+
(title prioritized, customer-over-company). System prompt assembled at runtime from the
|
|
89
|
+
`Team.*` prompt tables.
|
|
90
|
+
4. Apply DB replacements from `Team.TranscriptReplacements` via `preg_replace_callback`.
|
|
91
|
+
5. Build the approved name and upload the approved `.txt` + `{name}.txt.metadata.json` Bedrock
|
|
92
|
+
sidecar to `s3://togaiq/development-team/{kb_slug}/approved/` (us-east-1, togaiq account).
|
|
93
|
+
6. **Archive the raw VTT to `togaiq`** (`…/{kb_slug}/archive/{date}/`) **from the in-memory
|
|
94
|
+
Graph copy** — no `toga-private` round trip, no cross-account CopyObject.
|
|
95
|
+
7. Bedrock `startIngestionJob` on that KB's data source (retries on `ConflictException` /
|
|
96
|
+
throttling). `SyncKb` handles multi-slug lists; `syncBedrock()` now syncs a **single** KB
|
|
97
|
+
(the old "general fans out to all KBs" behavior was **intentionally removed**).
|
|
98
|
+
8. **Second AI pass** returns structured JSON, which **PHP renders to inline-styled HTML** (a
|
|
99
|
+
real `<table>` for Action Items) for a recap email. Subject is built in PHP:
|
|
100
|
+
`"Recap: {Meeting} - {Date}"`.
|
|
101
|
+
9. Enqueue `Notification/EmailTemplate/Send` to the organizer (see
|
|
102
|
+
[Background Email-Template Worker](./notification-email-template.md); the template is the
|
|
103
|
+
TOGA Technology `True`-client template).
|
|
104
|
+
|
|
105
|
+
### Organizer email resolution
|
|
106
|
+
- `resolveOrganizerEmail()` calls Graph `/users/{id}?$select=mail,userPrincipalName` — but
|
|
107
|
+
this needs the **`User.Read.All`** app permission, which the app registration **lacks**.
|
|
108
|
+
- **Reliable path = the FALLBACK:** `extractOrganizerUpn()` reads
|
|
109
|
+
`participants.organizer.upn` from the meeting. `getMeeting()` now requests
|
|
110
|
+
`$select=subject,startDateTime,participants`; the `OnlineMeetings` read scope already
|
|
111
|
+
exposes the organizer UPN, so **no extra permission** is needed.
|
|
112
|
+
|
|
113
|
+
### `SyncKnowledgeBases(bool $apply = true)` (cron)
|
|
114
|
+
Keeps `Team.KnowledgeBases` current with **Bedrock as source of truth**. Lists all Bedrock
|
|
115
|
+
KBs, filters to `{kb_prefix}*` (`development-team-`), derives the slug, upserts (new →
|
|
116
|
+
`isActive=1`; existing keep flags, refresh `bedrockKbId`); nothing is deleted. CLI:
|
|
117
|
+
`bin/sync-knowledge-bases.php --dry-run`. Cron `0 5 * * *` (daily 05:00 CT).
|
|
112
118
|
|
|
113
119
|
### `SyncKb(string $kbSlug)`
|
|
114
|
-
On-demand re-sync used by the Tools delete/move/edit flows
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`KB_SYNC_DELAY_SECONDS` spacing. This exists because **Bedrock allows only one in-flight
|
|
118
|
-
ingestion per KB** — a move that fired two independent parallel `SyncKb` workers raced and one
|
|
119
|
-
was dropped (only the destination synced). The Tools move now enqueues a **single**
|
|
120
|
-
`SyncKb("source,dest")` job so both KBs sync in one job, in order.
|
|
121
|
-
|
|
122
|
-
### `bin/reprocess-transcripts.php` (CLI backlog reprocessor)
|
|
123
|
-
Standalone CLI that bootstraps `_underscore`, lists existing raw transcripts under the
|
|
124
|
-
`toga-private` `transcripts/` prefix, and runs each through `Process()`. Flags
|
|
125
|
-
`--prefix` / `--limit` / `--dry-run`. Tolerates **both** the old `{slug}/{date}/` layout and the
|
|
126
|
-
new `{date}/` layout. Requires the `ENVIRONMENT` env var, e.g.
|
|
127
|
-
`ENVIRONMENT=production php bin/reprocess-transcripts.php --dry-run`.
|
|
128
|
-
|
|
129
|
-
### Graph download hardening
|
|
130
|
-
The Graph transcript download is hardened against SSRF / token leak: `https`-only, a
|
|
131
|
-
`*.microsoft.com` host allowlist, and `FOLLOWLOCATION` off.
|
|
120
|
+
On-demand re-sync used by the Tools delete/move/edit flows. `$kbSlug` accepts a
|
|
121
|
+
**comma-separated slug list** and syncs each KB **sequentially** with `KB_SYNC_DELAY_SECONDS`
|
|
122
|
+
spacing (Bedrock allows only one in-flight ingestion per KB, so parallel syncs race).
|
|
132
123
|
|
|
133
124
|
## Data model (`Team.*` schema, DB alias core cluster)
|
|
134
125
|
|
|
135
|
-
Authored in dbchanges2 (`Team/2026-06-30a..e`, `Core/2026-06-30a`, all dated 2026-06-30). All
|
|
136
|
-
PKs `INT UNSIGNED` to match the existing Talos Team-table family.
|
|
137
|
-
|
|
138
126
|
- **`Team.TranscriptReplacements`** — `pattern, replacement, isCaseSensitive, isWholeWord, isActive`.
|
|
139
|
-
|
|
140
|
-
- **`Team.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- **`Team.
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
candidates). NOTE: the Tools *browse* UI still derives its live KB list from S3 folders, not
|
|
150
|
-
this table.
|
|
151
|
-
- **`Team.TranscriptProcessing`** — `sourceKey UNIQUE(255), kbSlug, approvedKey, archiveKey,
|
|
152
|
-
status` ENUM (`pending/cleaned/uploaded/archived/synced/failed`), `aiModel, failureReason,
|
|
153
|
-
dtProcessed`. Pipeline ledger + idempotency.
|
|
154
|
-
- **`Core.CronJobs`** INSERT for action `Team/Transcripts/Scan` (weekday business hours);
|
|
155
|
-
includes `dtCreated = NOW()` since the column has no default, and depends on the
|
|
156
|
-
`maxExecutionTime` column migration.
|
|
127
|
+
- **`Team.TranscriptPromptTerms`** — `category` ENUM, `term, isActive, …`.
|
|
128
|
+
- **`Team.TranscriptPromptTemplate`** — `name, bodyText, instruction, model, temperature, …`.
|
|
129
|
+
- **`Team.KnowledgeBases`** — `slug, name, isGeneral, bedrockKbId, isActive`; auto-kept-current
|
|
130
|
+
from Bedrock by the daily `SyncKnowledgeBases`.
|
|
131
|
+
- **`Team.TranscriptProcessing`** — pipeline ledger + idempotency. `status` ENUM
|
|
132
|
+
(`pending/cleaned/uploaded/archived/synced/failed`). **`transcriptIdentifier` widened to
|
|
133
|
+
`VARCHAR(768)`** (dbchanges2 `Team/2026-07-09a`).
|
|
134
|
+
- **`Team.TranscriptExports`** — discovery ledger written by `Export`.
|
|
135
|
+
`transcriptIdentifier` **also widened to `VARCHAR(768)`**. Graph-direct columns +
|
|
136
|
+
`organizerEmail` added (dbchanges2 `Team/2026-07-08a`).
|
|
157
137
|
|
|
158
138
|
## Configuration
|
|
159
139
|
|
|
160
140
|
`worker2/Config/production.ini` `[talos]`: `s3_bucket=togaiq`, `s3_region=us-east-1`,
|
|
161
141
|
`bedrock_region=us-east-1`, `kb_prefix=development-team-`, `s3_root_path=development-team/`.
|
|
162
|
-
|
|
163
|
-
**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- `[talos]` `s3_access_key_id` / `s3_secret_access_key` — the **togaiq** key (approved/archive
|
|
167
|
-
KB bucket, us-east-1). Read by `getTalosS3Client()` and `getBedrockClient()`.
|
|
168
|
-
|
|
169
|
-
Credential **values live only in the deployed `production.ini`** — documented here by section
|
|
170
|
-
and account, never by value. Pre-existing plaintext keys should be rotated (separate track).
|
|
142
|
+
The `[talos]` key (togaiq account, us-east-1) is used for approved/archive writes, Bedrock,
|
|
143
|
+
**and now the archive of the raw VTT**. The `[teams]`/`toga-private` S3 key is **no longer
|
|
144
|
+
part of the ingestion loop** (raw reads removed). Credential values live only in the deployed
|
|
145
|
+
`production.ini` — documented here by section/account, never by value.
|
|
171
146
|
|
|
172
147
|
## Gotchas / known issues
|
|
173
148
|
|
|
174
|
-
- **
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
the
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
149
|
+
- **Idempotency: 304-char Graph ids truncated at `VARCHAR(255)` (root cause of endless
|
|
150
|
+
reprocessing).** Graph `callTranscript` ids are ~304 chars; `transcriptIdentifier` columns
|
|
151
|
+
were `VARCHAR(255)`, so ids were silently truncated. `alreadyExported()` compared the full
|
|
152
|
+
(304) id against the stored (255) prefix and **never matched**, AND the INSERT collided on
|
|
153
|
+
the truncated prefix (`error 1062 Duplicate entry … uq_transcript`) — so the same meeting
|
|
154
|
+
reprocessed on every poll. **Fixed by widening `transcriptIdentifier` to `VARCHAR(768)`** on
|
|
155
|
+
**both** `TranscriptProcessing` and the legacy `TranscriptExports`. 768 is the max for a
|
|
156
|
+
utf8mb4 single-column UNIQUE index (3072 bytes / 4).
|
|
157
|
+
- **Durability: dedupe-ledger writes must be SYNCHRONOUS + committed.** `recordExport()` and
|
|
158
|
+
the `loadOrCreateProcessing` INSERT had been dispatched **async** (see
|
|
159
|
+
[async query execution](../../_underscore/features/async-query-execution.md)), so the row
|
|
160
|
+
wasn't durable before the next poll → dedupe/watermark failed and meetings re-ran. These
|
|
161
|
+
pre-AI writes must run **synchronously** and `_Database::transactionCommit(_underscore::DB_TEAM)`.
|
|
162
|
+
Conversely the **post-AI `setStatus` writes stay async** — those run after long (up to 600s)
|
|
163
|
+
AI HTTP calls, which is exactly when the local Core connection has gone stale ("MySQL server
|
|
164
|
+
has gone away"). `alreadyExported()` must also `_Database::useQueryCache(false)` so it does
|
|
165
|
+
not read a stale empty result.
|
|
166
|
+
- **S3 filename safety: a `/` in a meeting title created an unintended S3 sub-folder.**
|
|
167
|
+
`filenameSafeTitle()` replaces `/` and `\` with `-` when building the approved/archive/
|
|
168
|
+
metadata S3 key; the **email subject keeps the untouched original title**. (Tools-side
|
|
169
|
+
`App_Talos_S3::sanitizeTitle()` was fixed to match — see
|
|
170
|
+
[Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md).)
|
|
171
|
+
- **KB registry split — a KB known to Bedrock/Client_True but not `Team.KnowledgeBases` is
|
|
172
|
+
silently rejected.** worker2's sync validates a KB slug against `Team.KnowledgeBases`
|
|
173
|
+
(`loadActiveKbSlugs`). A KB added only in Bedrock + `Client_True.VectorIndexes` (the registry
|
|
174
|
+
the newer Tools UI reads) is rejected by `SyncKb` as "unknown or inactive knowledge base" and
|
|
175
|
+
silently no-ops — the worker still returns HTTP-200, so the UI falsely reports "sync
|
|
176
|
+
initiated". **Fix:** run `Team/Transcripts/SyncKnowledgeBases` (or
|
|
177
|
+
`bin/sync-knowledge-bases.php`) to discover `development-team-*` KBs from Bedrock and upsert
|
|
178
|
+
them into `Team.KnowledgeBases`. **Open architectural point:** two competing registries —
|
|
179
|
+
`Team.KnowledgeBases` (worker2) vs `Client_True.VectorIndexes` (Tools UI).
|
|
180
|
+
- **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
|
|
181
|
+
`CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old
|
|
182
|
+
toga-private cross-account read is no longer in the loop.)
|
|
193
183
|
|
|
194
184
|
## Change history
|
|
195
185
|
|
|
196
|
-
- 2026-07-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
the
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
186
|
+
- 2026-07-09 — **Re-architected to GRAPH-DIRECT.** `Export` is now a thin cron poller
|
|
187
|
+
(removed the `organizers` param; added `$limit`, `cc`/`bcc`); `Scan` and
|
|
188
|
+
`bin/reprocess-transcripts.php` deleted; the `toga-private` staging bucket dropped entirely.
|
|
189
|
+
`Process` downloads the VTT straight from Graph, `stripAndGroupVtt()` (~40% token cut), and a
|
|
190
|
+
**second AI pass** now produces a PHP-rendered HTML recap email to the organizer
|
|
191
|
+
(`Notification/EmailTemplate/Send`, `True` template). Organizer email resolved via the
|
|
192
|
+
meeting `participants.organizer.upn` fallback (no `User.Read.All`). Widened
|
|
193
|
+
`transcriptIdentifier` to `VARCHAR(768)` on both ledgers (304-char Graph ids were truncated
|
|
194
|
+
at 255 → dedupe never matched + 1062 collision → endless reprocessing). Made the pre-AI
|
|
195
|
+
dedupe/discovery writes synchronous+committed (they had been async and weren't durable before
|
|
196
|
+
the next poll); post-AI status writes stay async. `filenameSafeTitle()` converts `/`,`\` →
|
|
197
|
+
`-` in S3 keys. `syncBedrock()` no longer fans "general" out to all KBs. dbchanges2:
|
|
198
|
+
`Team/2026-07-08a`, `Team/2026-07-09a`. (jcardinal)
|
|
199
|
+
- 2026-07-02 — Fixed cross-**account** S3 creds (per-bucket `[teams]`/`[talos]`, archive via
|
|
200
|
+
get+put); built `SyncKnowledgeBases` + `bin/sync-knowledge-bases.php` + daily cron; `SyncKb`
|
|
201
|
+
now takes a comma-separated slug list synced sequentially. Classification: title prioritized,
|
|
202
|
+
customer-over-company. Prompt template excludes personal/social content. (jcardinal)
|
|
203
|
+
- 2026-06-30 — Built the automated cron-driven ingestion pipeline porting the manual
|
|
204
|
+
`kb_processor.php`; added the `Team.*` config/ledger schema; classification moved to AI. (jcardinal)
|
|
211
205
|
|
|
212
206
|
## Related docs
|
|
213
207
|
|
|
214
|
-
- [Teams Meeting Transcript Export](./teams-transcript-export.md) —
|
|
208
|
+
- [Teams Meeting Transcript Export](./teams-transcript-export.md) — now merged into this
|
|
209
|
+
Graph-direct `Export` poller.
|
|
210
|
+
- [Background Email-Template Worker](./notification-email-template.md) — the recap email path.
|
|
211
|
+
- [Async Query Execution](../../_underscore/features/async-query-execution.md) — the sync-vs-async write rule.
|
|
215
212
|
- [Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md) — the Tools UI.
|
|
216
213
|
- [Talos Knowledge Base Pipeline](../../../1.0/apps/test/features/talos-kb-pipeline.md) — the manual script this ports.
|
|
217
214
|
- [Creating Worker Actions](./creating-worker-actions.md)
|
|
215
|
+
</content>
|
|
216
|
+
</invoke>
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: ["ajean", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Team/Transcripts.php
|
|
@@ -21,6 +21,16 @@ related:
|
|
|
21
21
|
|
|
22
22
|
## Summary
|
|
23
23
|
|
|
24
|
+
> **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin
|
|
25
|
+
> **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to `s3://toga-private/transcripts/`
|
|
26
|
+
> at all. It records a discovery-ledger row and enqueues one `Process` job per transcript, and
|
|
27
|
+
> `Process` downloads the VTT straight from Graph in-memory (raw is archived to `togaiq`, not
|
|
28
|
+
> toga-private). The `toga-private` staging bucket, the flat `transcripts/{date}/…` layout, and
|
|
29
|
+
> the `Backfill()` re-filer described here are no longer in the loop. See
|
|
30
|
+
> [Talos Transcript Ingestion](./talos-transcript-ingestion.md) for the current pipeline. The
|
|
31
|
+
> Graph organizer-resolution, GUID-vs-UPN, and Entra-permission material below is **still
|
|
32
|
+
> accurate** and remains the reference for how `Export` talks to Graph.
|
|
33
|
+
|
|
24
34
|
`_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for
|
|
25
35
|
Teams meeting transcripts produced by a set of organizers and archives the raw WebVTT to S3.
|
|
26
36
|
As of 2026-06-30 it **no longer classifies by client**: raw transcripts land **flat** at
|
|
@@ -171,6 +181,13 @@ Policy gap; a `400` only on a UPN means the id was never resolved to a GUID. (A
|
|
|
171
181
|
|
|
172
182
|
## Change history
|
|
173
183
|
|
|
184
|
+
- 2026-07-09 — **`Export` re-architected to GRAPH-DIRECT** (merged into the ingestion pipeline).
|
|
185
|
+
The `toga-private` staging bucket is dropped: `Export` is now a thin poller that records a
|
|
186
|
+
discovery row and enqueues one `Process` job per transcript; `Process` downloads the VTT
|
|
187
|
+
straight from Graph and archives the raw to `togaiq`. `Export` signature is now
|
|
188
|
+
`Export(int $lookbackDays, ?int $limit = null, array $cc = [], array $bcc = [])` (the
|
|
189
|
+
`organizers` param removed). The S3-layout / `Backfill()` sections above are historical. See
|
|
190
|
+
[Talos Transcript Ingestion](./talos-transcript-ingestion.md). (jcardinal)
|
|
174
191
|
- 2026-06-30 — **Removed title-based client classification and the client-folder S3 layout.**
|
|
175
192
|
Raw transcripts now land flat at `transcripts/{date}/…` with no client subfolder; deleted
|
|
176
193
|
`classify()`/`buildClientIndex()`/`loadClientNames()`/`loadClientAliases()` and the old
|
package/knowledge/INDEX.md
CHANGED
|
@@ -7,7 +7,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
7
7
|
- **library** (Library) _(framework core)_ — 11 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 14 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
10
|
-
- **togadesk** (TOGa Desk) —
|
|
10
|
+
- **togadesk** (TOGa Desk) — 9 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
11
11
|
- **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
12
12
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
13
13
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php, togadesk/desk/includes/classes/class.repair.php |
|
|
6
|
-
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php |
|
|
6
|
+
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php, library/app/netsuite.php |
|
|
7
7
|
| [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
|
package/package.json
CHANGED