toga-ai 1.0.342 → 1.0.344

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,11 +7,12 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-07-14
10
- owners: ["jcardinal"]
10
+ owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - _underscore/Cloud.php
13
13
  related:
14
14
  - ../../worker2/features/oneuptime-worker2-monitoring.md
15
+ - ../../worker2/features/compass-vip-support-importer.md
15
16
  ---
16
17
 
17
18
  ## Summary
@@ -58,8 +59,17 @@ None — shared core helper.
58
59
  do not add your own `ContinuationToken` loop on top.
59
60
  - A bucket outside the config-default us-east-1 (e.g. `agilant-as2` in us-west-2) throws
60
61
  `AuthorizationHeaderMalformed` unless you pass `$awsRegion`.
62
+ - **`getFileFromS3` (the GET method) is single-region.** It builds the `S3Client` with region
63
+ `_Config::cloud('aws_region')` (the worker's own region) and only swallows HTTP 404. Reading
64
+ an object from a bucket in a **different** region raises a `301 PermanentRedirect`, which it
65
+ re-throws. To read a cross-region (or public) S3 object, use a direct HTTPS download instead,
66
+ or construct an `S3Client` with the bucket's own region. (Unlike `getS3Objects()`, `getFileFromS3`
67
+ has no region-override parameter.)
61
68
 
62
69
  ## Change history
70
+ - 2026-07-15 — Documented that `getFileFromS3` is single-region (client built with the worker's
71
+ own `aws_region`, only swallows 404, re-throws `301 PermanentRedirect` for cross-region reads);
72
+ use a direct HTTPS download for cross-region/public objects. (bala)
63
73
  - 2026-07-14 — `getS3Objects()` gained an optional `?string $awsRegion = null` 3rd param
64
74
  (falls back to `_Config::cloud('aws_region')`); needed because `agilant-as2` is in
65
75
  us-west-2, not the config-default us-east-1. Additive — 2-arg callers unchanged. (jcardinal)
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
10
- owners: [jcardinal]
9
+ updated: 2026-07-15
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Core/Surface.php
@@ -61,7 +61,27 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
61
61
  `Client.AclRecordScripts` dispatch grant — it 403'd EZ-1 on dev-sandbox until granted, exactly like
62
62
  `meta` did. See [surface-resolver](../../_underscore/features/surface-resolver.md).
63
63
 
64
+ - **The tell that it's the dispatch-grant gate (not record-level READ) failing: the 403 EZ-1
65
+ response carries `identifiers.script` set to the script route** (e.g. `"meta"`). If you see
66
+ `identifiers.script`, chase `AclRecordScripts`, not `AclRecordPermissions`.
67
+ - **Which roles the dispatch check reads depends on the record's `aclDatabase`.** The `surfaces`
68
+ Record has `aclDatabase = CORE`, so `getRecordScriptPhpMethod` matches the caller's **CORE** roles
69
+ (`Core.Roles`: 1=Public, 2=Integrations, 3=Super User, 4=Base) against the `AclRecordScripts` rows —
70
+ even though those rows physically live in each **client** DB. The public login surface authenticates
71
+ as CORE Public(1). (⚠ This corrects the earlier "keyed on client roles" note in
72
+ [surface-resolver](../../_underscore/features/surface-resolver.md) — the two disagree; confirm
73
+ against `V2.php getRecordScriptPhpMethod` before relying on either.)
74
+ - **Seed the dispatch grants in EVERY client DB.** Grant pattern: `meta` + `meta-group` →
75
+ Public(1)/Super User(3)/Base(4); `debug` → Super User(3) only. A client DB missing the grant 403s
76
+ that tenant even though the code and Core metadata are identical — new/pre-existing client DBs
77
+ silently lack it.
78
+
64
79
  ## Change history
80
+ - 2026-07-15 — Documented the diagnostic tell (`identifiers.script` present on the 403 EZ-1 → it is
81
+ the `AclRecordScripts` dispatch gate, not record READ); clarified the dispatch check reads the
82
+ caller's **CORE** roles because `surfaces.aclDatabase = CORE` (contradicts the earlier "client
83
+ roles" note — flagged for reconciliation); and that the per-role grants (`meta`/`meta-group` →
84
+ 1/3/4, `debug` → 3 only) must be seeded in every client DB. (apeterson)
65
85
  - 2026-06-30 — Noted the grouped `meta-group` endpoint (batched multi-slug resolve) and that it needs
66
86
  its own per-role `AclRecordScripts` dispatch grant (403 EZ-1 until granted). (jcardinal)
67
87
  - 2026-06-29 — Documented the second ACL layer: scripted-API dispatch is authorized via
@@ -11,6 +11,7 @@
11
11
  | [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | ClickUp custom text fields (type `text` and long-text) support rich formatting only through a **Quill Delta** written to the undocumented `value_richtext` key o | test/@dave/clickup_md2delta.js, .claude/skills/plan-ticket/scripts/clickup.js |
12
12
  | [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity** or **Epic** it rolls up to, so a deal/project owner sees activity whe | worker2/Worker/Clickup/Subtask.php, worker2/Worker/Clickup.php, dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql |
13
13
  | [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
14
+ | [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP user's support technician by setting `Users.c_supportedByUserId` in `Clie | worker2/Worker/Client/Compass/VipSupport.php |
14
15
  | [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 |
15
16
  | [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 |
16
17
  | [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: Compass VIP Support Importer (worker2)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-07-15
10
+ owners: [bala]
11
+ files:
12
+ - worker2/Worker/Client/Compass/VipSupport.php
13
+ related:
14
+ - ./creating-worker-actions.md
15
+ - ../../../clients/compass-usa/profile.md
16
+ - ../../_underscore/features/cloud-s3-helpers.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP
22
+ user's support technician by setting `Users.c_supportedByUserId` in `Client_Compass`. It is
23
+ invoked **manually via Postman** (no cron). Given a file, it stages one update per matching
24
+ VIP row, applies them in a single transaction, and returns a JSON report of what was updated
25
+ and — with a per-row reason — what was skipped.
26
+
27
+ - **Action:** `Client/Compass/VipSupport/ProcessVipFile`
28
+ - **Params:** `{"filePath": "<local path OR http(s) URL>"}`
29
+
30
+ ## Key files / entry points
31
+
32
+ - `worker2/Worker/Client/Compass/VipSupport.php` — `ProcessVipFile(string $filePath): string`.
33
+
34
+ ## How it works
35
+
36
+ 1. **Locate the input.** `filePath` may be a local filesystem path **or** an `http(s)` URL.
37
+ A URL (e.g. a public S3 object) is downloaded via cURL to a temp file first (see
38
+ *Remote input* below); a local path is used as-is.
39
+ 2. **Find the header row.** Compass now prefixes the sheet with a **title-banner row above
40
+ the header**, so the reader does not assume row 1. `findHeaderRowNumber()` scans for the
41
+ row whose first cell equals `PERS. NO.` and uses that as the header; the banner above it
42
+ is ignored.
43
+ 3. **Validate headers.** The header row is checked against the 12 `EXPECTED_HEADERS`
44
+ (`PERS. NO.`, `FIRST NAME`, `LAST NAME`, `USERNAME`, `EMAIL ADDRESS`, `POSITION`,
45
+ `COST CTR`, `SECTOR`, `COUNTRY`, `LOCATION`, `SUPPORT TECH`, `Support Tech Email`).
46
+ A missing/reordered column throws.
47
+ 4. **Load users.** `loadUserMaps()` loads **every** user (active + inactive) from
48
+ `Client_Compass.Users` into two maps keyed by lowercased `c_hrEmpUsername` and `email`,
49
+ carrying the `isActive` flag. `ORDER BY isActive DESC` means an active row wins a
50
+ duplicate key, and a skip can distinguish "inactive" from "not in the DB".
51
+ 5. **Evaluate each data row.** For every row below the header, `evaluateVipRow()` (guard
52
+ clauses / early return — no `continue`) returns either a staged update
53
+ `{id, c_supportedByUserId}` or a structured skip `{row, user, reason}`.
54
+ 6. **Stop at the first fully-empty row.** `isEntirelyEmptyRow()` (all 12 columns empty) marks
55
+ the end of the list; the loop breaks and reports `stoppedAtRow`. Rows below it (including a
56
+ trailing legend/note row) are never processed. A *partial* row (only a support tech, or a
57
+ note in one column) is **not** a stop.
58
+ 7. **Apply.** Staged updates are applied in batches inside one transaction
59
+ (`buildBatchUpdateSql` — a `CASE id WHEN … THEN …` update); rollback on any failure.
60
+ 8. **Report.** Returns JSON `{updated, skipped, stoppedAtRow, skippedRows[]}`. There is no
61
+ echo logging — the returned JSON is the entire output.
62
+
63
+ ### Matching keys
64
+
65
+ - VIP identity → `Users.c_hrEmpUsername` (matched against the file's `USERNAME` column).
66
+ - Support technician → `Users.email` (matched against the file's `Support Tech Email` column).
67
+
68
+ ### Skip reasons
69
+
70
+ Each `skippedRows[]` entry carries the **real spreadsheet row number** (`$row->getRowIndex()`,
71
+ so it lines up with what you see in Excel — not an internal counter offset by the banner/header)
72
+ plus a reason: VIP not in the DB · VIP inactive · VIP has no username in the file (with an
73
+ `(and no support tech email)` note when applicable) · row has no VIP identity to match ·
74
+ support tech email not in the DB · support tech inactive.
75
+
76
+ ### Remote input
77
+
78
+ `isRemoteUrl()` detects an `http(s)` URL; `downloadToTempFile()` fetches it via cURL
79
+ (`DOWNLOAD_TIMEOUT_SECONDS = 60`, `FAILONERROR`, follow redirects) to a temp file **kept with
80
+ an `.xlsx` suffix** so PhpSpreadsheet's `IOFactory` resolves the reader by extension. The temp
81
+ file is **always removed in a `finally` block**. A local input file is unlinked after a
82
+ successful run (unless `TEST_MODE`).
83
+
84
+ ## Client variations
85
+
86
+ Compass-only. Some spreadsheet rows reference users that belong to a **different Compass tenant
87
+ DB** and are inactive in `Client_Compass` — those rows are skipped with the "not active"
88
+ reason, which is expected, not an error.
89
+
90
+ ## Gotchas / known issues
91
+
92
+ - **PhpSpreadsheet `RowIterator` rewinds on `foreach`.** `RowIterator::rewind()` resets to
93
+ `startRow`, and `foreach` always rewinds. An earlier version advanced past the header with
94
+ manual `next()`/`current()`, but the subsequent `foreach` rewound the iterator, so the header
95
+ row was silently reprocessed as a (skipped) data row. Fix: use explicit ranges —
96
+ `getRowIterator($headerRowNum, $headerRowNum)` for the header and
97
+ `getRowIterator($headerRowNum + 1)` for data.
98
+ - **The file format changes.** The current Compass standard is **12 columns** with a banner row
99
+ above the header. The previous format was 14 columns (header on row 1) with `Support Tech
100
+ First`/`Support Tech Last` columns; removing those moved `Support Tech Email` from column N
101
+ (index 13) to column L (index 11). If Compass changes the layout again, adjust
102
+ `EXPECTED_COL_COUNT`, `EXPECTED_HEADERS`, `IDX_SUPPORT_EMAIL`, and the `'A'..'L'` cell
103
+ iterators together.
104
+ - **Why a URL is preferred over a `/tmp` path in production.** The prod EB worker tier runs
105
+ **more than one instance** and SQS distributes jobs across them, so a file placed in `/tmp`
106
+ on one instance is invisible to the instance that actually runs the job — a local path fails
107
+ intermittently with "file not found". A URL fetched at runtime works regardless of which
108
+ instance runs the job. (Running-app path is `/var/app/current`, even though an SSH session on
109
+ the AL2023 box shows `/var/www/html`.)
110
+ - **Cross-region / public S3 reads.** `_Cloud::getFileFromS3` is single-region and cannot read
111
+ a bucket in another region — this importer downloads the object over plain HTTPS instead. See
112
+ [_Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md).
113
+
114
+ ## Change history
115
+ - 2026-07-15 — Adapted to Compass's new 12-column format (banner row above the header;
116
+ `Support Tech First`/`Support Tech Last` removed, so `Support Tech Email` moved col N→L);
117
+ added `findHeaderRowNumber()` scanning for `PERS. NO.` and switched to explicit
118
+ `getRowIterator` ranges to fix the RowIterator-rewind bug that silently reprocessed the
119
+ header as a data row. (bala)
120
+ - 2026-07-15 — `ProcessVipFile` now accepts an `http(s)` URL (downloaded via cURL to a temp
121
+ `.xlsx`, always removed in `finally`), because the prod EB worker runs multiple instances and
122
+ a `/tmp` file on one is invisible to the instance that runs the job. (bala)
123
+ - 2026-07-15 — Added structured per-row skip reporting (real spreadsheet row numbers; reasons
124
+ distinguishing inactive vs. not-in-DB), all-users-with-`isActive` load, and stop-at-first-
125
+ empty-row; `ProcessVipFile` returns JSON and echo logging was removed. (bala)
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
  ## 2.0 framework
19
19
 
20
20
  - **_underscore** (_Underscore) _(framework core)_ — 32 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
- - **worker2** (Worker) — 29 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
21
+ - **worker2** (Worker) — 30 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
24
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -15,13 +15,14 @@ project: _Underscore
15
15
  client: compass-usa
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-07-08
18
+ updated: 2026-07-15
19
19
  owners: [jcardinal, bala, tcox, apeterson]
20
20
  files: []
21
21
  related:
22
22
  - features/asn-to-item-fulfillment.md
23
23
  - features/cost-centers.md
24
24
  - workflows/cross-kit-bundle-corruption.md
25
+ - ../../2.0/apps/worker2/features/compass-vip-support-importer.md
25
26
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
26
27
  - ../../2.0/apps/toga2-commerce/features/cart-bundle-submission-and-identity.md
27
28
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
@@ -71,6 +72,9 @@ separate, related client (see its own profile).
71
72
  - [Cost Centers (Unit Locations, numeric-only)](features/cost-centers.md) — what a Compass
72
73
  "cost center" actually is (a Unit Location keyed by `c_erpSystemEntityId`), and the
73
74
  numeric-only selection/import/data policy (2026-07-06).
75
+ - [VIP Support Importer (worker2)](../../2.0/apps/worker2/features/compass-vip-support-importer.md)
76
+ — manual (Postman) worker2 action that reads Compass's quarterly VIP spreadsheet and sets
77
+ `Users.c_supportedByUserId` (assigned support tech) per VIP.
74
78
 
75
79
  ## Notes
76
80
  - **Order status is shipped-only (2026-06-30).** Compass imports all IF stages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.342",
3
+ "version": "1.0.344",
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",