toga-ai 1.0.389 → 1.0.391

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.
@@ -4,7 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
6
6
  | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
7
- | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
7
+ | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
8
8
  | [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
9
9
  | [_ApiRequest ENCODE__JSON now sends Content-Type: application/json](features/apirequest-json-content-type.md) | `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set a `Content-Type` header. | _underscore/ApiRequest.php |
10
10
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
@@ -6,11 +6,12 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-07-21
10
10
  owners: ["jcardinal", "mhammontree"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Core/Page.php
14
+ - _underscore/Model/Client/TrackingNumber.php
14
15
  - dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
15
16
  - dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
16
17
  ---
@@ -112,10 +113,11 @@ Worked example: the reprint API `GET /v2/tracking-numbers/reprint`
112
113
  Client-DB `AclRecordScripts` grant (migrations in `dbchanges2`) before it stopped 403ing — see the
113
114
  carrier-shipping-labels feature doc.
114
115
 
115
- ## Add a writable field to a V2 record (3-file migration)
116
+ ## Add a writable field to a V2 record (FOUR requirements)
116
117
 
117
- Adding a persisted, API-writable field to an existing V2 record is a **three-file migration set**
118
- — miss any one and the API rejects the write with a specific code (see the
118
+ Making a field persisted **and** API-writable on an existing V2 record takes **four** things —
119
+ three migration files plus a code change in the generated model. Miss any one and the API
120
+ rejects the write with a **specific code** (see the
119
121
  [V2 API error codes](../../api2/features/v2-api-error-codes.md) map):
120
122
 
121
123
  1. **Core `RecordFields` registration** (`dbchanges2/Core/`) — register the field name + type
@@ -130,11 +132,30 @@ Adding a persisted, API-writable field to an existing V2 record is a **three-fil
130
132
  writable sibling field** on the same record — pick a sibling that actually has explicit
131
133
  `AclFieldPermissions` rows (e.g. on `TrackingNumbers`, `containsBattery` has them, whereas
132
134
  `requiresSignature` had none), and resolve `roleId` by subselect.
135
+ 4. **Generated model-class declaration** (`_underscore/Model/Client/<Name>.php`) — the field must
136
+ be declared on the **generated** PHP model class, e.g.
137
+ `public $needsReturnLabel = self::FIELD_BOOLEAN;`. **Without it the API returns `EO-1`**
138
+ ("There is no field called 'X' in the '_Model_Client_Y' model"). This is a **code**
139
+ requirement, not a migration, so it is the one most easily missed when the DB side is already
140
+ complete — see the branch-mismatch gotcha below.
133
141
 
134
142
  Worked example (TRUE-79191, `TrackingNumbers.signatureType`): `Core/... - TrackingNumberSignatureTypeRecordField.sql`
135
143
  (RecordField, `STRING`), `Client/... - TrackingNumberSignatureType.sql` (guarded `ALTER TABLE`),
136
- `Client/... - TrackingNumberSignatureTypeFieldPermission.sql` (cloned `containsBattery`'s grants).
137
- This mirrors the earlier `needsReturnLabel` / `returnAddressId` 3-file sets.
144
+ `Client/... - TrackingNumberSignatureTypeFieldPermission.sql` (cloned `containsBattery`'s grants),
145
+ plus the `signatureType` declaration on `_underscore/Model/Client/TrackingNumber.php`. This
146
+ mirrors the earlier `needsReturnLabel` / `returnAddressId` sets.
147
+
148
+ **⚠ EO-1 is usually a cross-repo git BRANCH MISMATCH, not a DB gap.** An `EO-1` "no field called
149
+ 'X' in the model" with the DB column + `Core.RecordFields` both present means the field isn't in
150
+ the generated model class the running `_underscore` is loading — most often because the repos are
151
+ on **different branches** (`_underscore` on a branch whose generated `Model/Client/<Name>.php`
152
+ predates the field the DB already has). **Fix:** put all the feature's repos (`_underscore`,
153
+ `api2`, `dbchanges2`, `toga2-supply`) on the **same** feature branch before chasing migrations.
154
+ On a local dev box with PHP opcache disabled, a branch switch takes effect immediately (files are
155
+ read fresh per request) — no Apache restart needed. Concrete case: `TrackingNumbers.needsReturnLabel`
156
+ (migrations `2026-07-02b` Client column + Core RecordField, `2026-07-02c` client AclFieldPermission)
157
+ threw EO-1 purely because `_underscore` was on a branch whose generated `TrackingNumber.php` lacked
158
+ the field.
138
159
 
139
160
  ## Checklist (so the chain is never half-built)
140
161
 
@@ -144,9 +165,18 @@ This mirrors the earlier `needsReturnLabel` / `returnAddressId` 3-file sets.
144
165
  - [ ] `AclLogicGroupExpressions` row binding the group to the expression.
145
166
  - [ ] `AclFieldPermissions` rows for every field that must be readable/writable.
146
167
  - [ ] For `aclDatabase = 'CLIENT'` records: rows go in client DB(s); resolve `roleId` by subselect.
168
+ - [ ] The field is declared on the **generated** `_underscore/Model/Client/<Name>.php` (else `EO-1`),
169
+ and every repo is on the **same branch** so the generated model matches the DB.
147
170
 
148
171
  ## Change history
149
172
 
173
+ - **2026-07-21** — TRUE-79191: promoted the writable-field recipe from **3-file** to **FOUR
174
+ requirements** — added the **generated model-class declaration** (`_underscore/Model/Client/<Name>.php`,
175
+ e.g. `public $needsReturnLabel = self::FIELD_BOOLEAN;`) as the 4th, whose absence returns **`EO-1`**
176
+ ("no field called 'X' in the '_Model_Client_Y' model"). Documented that EO-1 with a fully migrated DB
177
+ is usually a **cross-repo git branch mismatch** (`_underscore` on a branch whose generated model
178
+ predates the field) — fix is to align all fulfill/ship repos on one branch (opcache-disabled dev
179
+ picks up the switch immediately). Concrete: `TrackingNumbers.needsReturnLabel`. (mhammontree)
150
180
  - **2026-07-17** — TRUE-79191: added the **"add a writable field to a V2 record" 3-file migration**
151
181
  recipe (Core `RecordFields` → `EV-8` if missing; guarded Client column; Client `AclFieldPermissions`
152
182
  → `EV-9` if missing, cloned from a writable sibling like `containsBattery`), worked example
@@ -12,6 +12,6 @@
12
12
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
13
13
  | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql |
14
14
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
15
- | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php |
15
+ | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TrackingNumber.php |
16
16
  | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
17
17
  | [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, dbchanges2/Logs/, _underscore/Route.php |
@@ -6,10 +6,11 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-17
9
+ updated: 2026-07-21
10
10
  owners: [mhammontree]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
+ - _underscore/Model/Client/TrackingNumber.php
13
14
  related:
14
15
  - ../../_underscore/features/acl-permission-chain.md
15
16
  - scripted-api-post-body-args.md
@@ -33,6 +34,7 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
33
34
  | **EV-6** | Resource / route not found | A **scripted-API** route segment is being treated as a record uuid because no Record Script matches — i.e. the `Core.RecordScripts` route row is missing | Register the `Core.RecordScripts` row (`recordId`, `method`, `route`, `phpMethod`) |
34
35
  | **EV-8** | Request field does not exist | A field sent in the payload isn't registered as a **`Core.RecordFields`** row for that record | Register the field in `Core.RecordFields` (see the "add a field to a V2 record" recipe in the ACL doc) |
35
36
  | **EV-9** | No permission to write field | The field exists but the caller's role has no **`AclFieldPermissions`** grant (`isWritable=1`) in the CLIENT DB | Add the `AclFieldPermissions` row for the role (clone a writable sibling field's grant) |
37
+ | **EO-1** | Operation failed — identifier "There is no field called 'X' in the '_Model_Client_Y' model" | The DB column **and** `Core.RecordFields` exist, but the **generated model class** `_underscore/Model/Client/<Name>.php` doesn't declare the field. This is the **4th** requirement beyond the 3-file migration — and most often it's a **cross-repo git branch mismatch** (`_underscore` on a branch whose generated model lacks a field the DB/RecordFields already carry) | Declare the field in the generated model class (`public $field = self::FIELD_*`) and put all related repos (`_underscore`, `api2`, `dbchanges2`, `toga2-supply`) on the **same** feature branch — see the ACL doc's writable-field recipe |
36
38
  | **EZ-1** | Unauthorized record/script dispatch | Missing **`AclRecordScripts`** (scripted APIs) or the **`AclRecordPermissions`** four-table chain (records) for the caller's role | Grant `AclRecordScripts` (scripts) or complete the record-CRUD chain |
37
39
  | **EV-5** | Duplicate `transactionId` | The globally-unique `transactionId` was reused | Send a fresh unique `transactionId` per request |
38
40
  | **EV-12** | Record's parent not synced | (Fulfill & Ship) POST `/item-fulfillments` when the Sales Order isn't synced into Toga yet | `GET /sales-orders/syncNetsuiteSalesOrder?netsuiteInternalSalesOrderId=<id>` first |
@@ -48,6 +50,13 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
48
50
  3. **EV-6 vs EZ-1** are the two halves of exposing a scripted API: EV-6 = no `Core.RecordScripts`
49
51
  route (the segment falls through to record lookup); EZ-1 = the route exists but there's no
50
52
  `AclRecordScripts` dispatch grant for the caller's role.
53
+ 4. **EO-1 ("no field called 'X' in the model") is NOT a DB problem.** EV-8 means the Core
54
+ `RecordFields` registration is missing; EO-1 means the DB column and RecordFields are both
55
+ present but the **generated PHP model class** lacks the field. Before touching migrations,
56
+ confirm every fulfill/ship repo is on the **same branch** — a mismatched `_underscore`
57
+ branch (whose generated `Model/Client/<Name>.php` predates the field) reproduces EO-1 with the
58
+ DB fully migrated. On a local dev box with PHP opcache disabled a branch switch takes effect
59
+ immediately (files are read fresh per request) — no Apache restart needed.
51
60
 
52
61
  ## Related recipes
53
62
 
@@ -62,5 +71,9 @@ migration instead of re-deriving it. All field/script ACL rows live in the **CLI
62
71
  discovered wiring the Signature Type field and the `addresses/validate` scripted route on Fulfill
63
72
  & Ship — each code mapped to the missing migration (RecordField / AclFieldPermissions /
64
73
  RecordScripts / AclRecordScripts) and the resolve-role-by-sibling technique. (mhammontree)
65
- </content>
66
- </invoke>
74
+ - 2026-07-21 — TRUE-79191: added **EO-1** ("There is no field called 'X' in the '_Model_Client_Y'
75
+ model") to the map — the DB column + `Core.RecordFields` are present but the **generated model
76
+ class** `_underscore/Model/Client/<Name>.php` doesn't declare the field (the 4th requirement
77
+ beyond the 3-file migration), most often a **cross-repo git branch mismatch**; fix is to align
78
+ all fulfill/ship repos on one branch. Concrete: `TrackingNumbers.needsReturnLabel`. Also fixed
79
+ stray tag text at end of file. (mhammontree)
@@ -9,3 +9,4 @@
9
9
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
10
10
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
11
11
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
12
+ | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/src/utils/getFontAwesomeIcon.tsx |
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Dynamic npm Publish Pipeline (branch → channel)
3
+ framework: "2.0"
4
+ repo: toga-blox
5
+ project: TOGa Blox
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-07-21
10
+ owners: [jcardinal]
11
+ files:
12
+ - toga-blox/.github/workflows/publish.yml
13
+ - toga-blox/src/utils/getFontAwesomeIcon.tsx
14
+ related:
15
+ - ../architecture.md
16
+ - ../../toga2-commerce/workflows/amplify-build-and-deploy.md
17
+ ---
18
+
19
+ ## What it is
20
+
21
+ How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`)
22
+ publishes a per-environment npm **channel** (dist-tag) from a `_<mode>` release branch. As of
23
+ 2026-07-21 the `publish.yml` workflow is **fully dynamic** — there is no per-branch hardcoded
24
+ logic. Use this doc when adding a new environment channel, debugging a channel that won't
25
+ resolve in a consuming app's build, or touching `publish.yml`.
26
+
27
+ ## How it works
28
+
29
+ ### Branch → channel (1:1, load-bearing)
30
+
31
+ - Trigger is `on.push.branches: ['_*']` — **any** underscore-prefixed branch publishes.
32
+ - The npm dist-tag (channel) is derived by stripping the one leading underscore from the
33
+ branch name: pushing `_sandbox-client` publishes `@agilant/toga-blox@sandbox-client`.
34
+ Branch mode == channel, strictly one-to-one.
35
+ - **Every** branch (including `_production`) publishes a **prerelease** version
36
+ `X.Y.Z-<mode>.<run>` under dist-tag `<mode>`. There is no longer a production special case.
37
+ Verified live: `production:1.0.315-production.96`, `sandbox-client:1.0.315-sandbox-client.97`.
38
+
39
+ ### What was removed (and why it's safe)
40
+
41
+ - The old workflow hardcoded a branch list (`_production/_gamma/_beta/_qc-security`) with a
42
+ production special case: `_production` published dist-tag `latest` with a plain (non-prerelease)
43
+ version, committed the version bump back, and git-tagged. **All of that is gone.**
44
+ - **Consequence:** the workflow no longer maintains the `@latest` npm tag. This is fine because
45
+ consumers never install bare `npm install @agilant/toga-blox`; they install by explicit
46
+ `@<mode>` channel (commerce's `amplify.yml`) or by a pinned/ranged version in `package.json`.
47
+
48
+ ### The cross-repo publish-then-build model (key mental model)
49
+
50
+ A GitHub branch and an npm channel are **unrelated**. Creating a `_<mode>` branch in a *consuming
51
+ app* repo (e.g. `toga2-commerce`) only tells Amplify what to build — it does **not** create a
52
+ blox channel. A channel exists **only** when this repo (`toga-blox-npm`) publishes with
53
+ `npm publish --tag <mode>`, which `publish.yml` now does automatically for any `_<mode>` branch.
54
+
55
+ **Correct order to stand up a new environment:**
56
+ 1. Create + push `_<mode>` in `toga-blox-npm` → channel publishes. Verify with
57
+ `npm view @agilant/toga-blox dist-tags`.
58
+ 2. Then the app's `_<mode>` Amplify build succeeds, because `npm install @agilant/toga-blox@<mode>`
59
+ resolves.
60
+
61
+ This publish-before-build ordering is the ongoing cost of the strict 1:1 model: every
62
+ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
63
+
64
+ ## Gotchas
65
+
66
+ - **Publish before build.** `npm install @agilant/toga-blox@<mode>` fails `ETARGET No matching
67
+ version` in the app build until this repo has published that channel. GitHub Actions runs the
68
+ workflow version *on the pushed branch*, so the branch must carry the current `publish.yml`.
69
+ - **FontAwesome v7 `style`-prop TS2322** (fixed 2026-07-21 in `getFontAwesomeIcon.tsx`, and a
70
+ reusable pattern for any consumer). FA v7 types the `FontAwesomeIcon` `style` prop as
71
+ `CSSProperties & CSSVariables`, where `CSSVariables` requires a `--fa-*` custom-property index
72
+ signature that plain `React.CSSProperties` lacks. Cast the passed style to
73
+ `React.CSSProperties & Record<\`--${string}\`, string>` — the value type must be `string`
74
+ (**not** `string | number`; `CSSVariables` requires `string`). Runtime and function signature
75
+ are unchanged, so it is safe for the ~40 consumers.
76
+ - **Secret hygiene done right.** `publish.yml` writes a transient `.npmrc` at build time from
77
+ GitHub secrets (`${NODE_AUTH_TOKEN}` / FontAwesome token) — it never commits tokens. This is
78
+ the pattern the consuming apps should follow (see the commerce workflow's secret-hygiene note).
79
+
80
+ ## Change history
81
+ - 2026-07-21 — Made `publish.yml` fully dynamic: trigger `_*`, branch→dist-tag by stripping the
82
+ leading underscore (mode == channel 1:1), every branch (incl. `_production`) publishes a
83
+ `X.Y.Z-<mode>.<run>` prerelease; removed the `_production`→`latest` special case (workflow no
84
+ longer maintains `@latest`); resolved a stash/merge conflict by keeping the dynamic path and
85
+ dropping the redundant hardcoded `_qc-security` case. Fixed `getFontAwesomeIcon.tsx` FA v7
86
+ TS2322. Documented the cross-repo publish-then-build model. (jcardinal)
87
+ </content>
88
+ </invoke>
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-06-18
9
+ updated: 2026-07-21
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - toga2-commerce/amplify.yml
@@ -15,50 +15,52 @@ files:
15
15
  - toga2-commerce/.github/workflows/sync-stage-environments.yml
16
16
  related:
17
17
  - ../../toga2-supply/workflows/amplify-build-and-deploy.md
18
+ - ../../toga-blox/workflows/dynamic-publish-pipeline.md
18
19
  ---
19
20
 
20
21
  ## Summary
21
22
 
22
23
  How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**.
23
- Same branch-per-environment model as `toga2-supply` (see related doc), with one important
24
- difference: commerce's build scripts **install a per-environment `@agilant/toga-blox` channel**
25
- before building, so the Amplify build must run the npm scripts (not a raw `vite build`), and the
26
- matching blox dist-tag must exist on npm first.
27
-
28
- ## Deployment model — one branch per environment (mirrors toga2-supply)
29
-
30
- - Branches are **underscore-prefixed** (`_production`, `_beta`, `_gamma`, `_qc-security`); the
31
- Vite mode is the same name **without** the underscore.
32
- - Repo-root `amplify.yml` drives every branch's build. It sets
33
- `NODE_OPTIONS=--max-old-space-size=8192`, strips the leading `_` from `$AWS_BRANCH` to get the
34
- mode, **fails loudly if `.env.<mode>` is missing**, then maps the mode → the matching npm build
35
- script via a `case` (production→`npm run build`, alpha→`buildAlpha`, beta→`buildBeta`,
36
- gamma→`buildGamma`, qc-security→`buildQcSecurity`; unmapped → `exit 1`). Single-line command, LF
37
- only (`.gitattributes` pins `*.yml` to `eol=lf`).
24
+ Same single-shared-`amplify.yml`, branch-per-environment model as `toga25-supply` (mode derived
25
+ from `AWS_BRANCH`), but with commerce-specific build constraints preserved: it **dynamically
26
+ installs a per-environment `@agilant/toga-blox` channel** (channel == mode, strict 1:1), needs a
27
+ larger heap, and pulls FontAwesome Pro. As of 2026-07-21 the build is **inline and fully dynamic**
28
+ (no per-branch `case`, no `npm run <script>`), so one DRY spec serves all ~21 environments.
29
+
30
+ ## Deployment model — one dynamic `amplify.yml` for every branch
31
+
32
+ - Branches are **underscore-prefixed** `_<mode>`; the Vite mode is the same name **without** the
33
+ underscore, derived at build time from `$AWS_BRANCH`. **Fails loudly** if the branch lacks the
34
+ leading underscore or `.env.<mode>` is missing — a misconfigured branch can never silently ship
35
+ as production. Single-line commands, LF only (`.gitattributes` pins `*.yml` to `eol=lf`).
36
+ - The build is **inline in `amplify.yml`**, NOT `npm run <script>`, so the per-mode `package.json`
37
+ build scripts are untouched. Steps: dynamic `npm install "@agilant/toga-blox@$MODE"
38
+ --legacy-peer-deps`, FontAwesome Pro install, `npm install` (not `npm ci`) `--legacy-peer-deps`,
39
+ `npx tsc -b` (tsconfig is composite / project references), then `npx vite build --mode "$MODE"`.
40
+ `node_modules` is cached.
41
+ - **`NODE_OPTIONS="--max-old-space-size=8192"` must be re-exported in the `build` phase**, not
42
+ just `preBuild` — the memory-heavy Rollup step runs in `build`, and Amplify may use a separate
43
+ shell per phase, so the default heap OOMs otherwise.
38
44
  - `sync-stage-environments.yml` fans `_stage` → `_qc-security` on push.
39
45
 
40
46
  ## blox per-environment channel + the publish chain
41
47
 
42
- Unlike supply (which pins blox via a normal dependency), each commerce build script installs a
43
- **blox dist-tag** matching the environment:
44
- `"buildQcSecurity": "npm install @agilant/toga-blox@qc-security --legacy-peer-deps && tsc && vite build --mode qc-security"`.
48
+ Unlike supply (which pins blox via a normal dependency), commerce installs a **blox dist-tag
49
+ matching the mode**: `npm install "@agilant/toga-blox@$MODE" --legacy-peer-deps` — no hardcoded
50
+ channel list, resolved from `$MODE` at build time.
45
51
 
46
- Those dist-tags are produced by the **`agilantsolutions/toga-blox-npm`** repo's
47
- `.github/workflows/publish.yml`, which auto-publishes on push to specific branches and maps
48
- **branch → dist-tag** (one-to-one, underscore stripped): `_production`→`latest`, `_beta`→`beta`,
49
- `_gamma`→`gamma`, and (added 2026-06-18) `_qc-security`→`qc-security`. So enabling a new channel
50
- is a two-part change: (1) add the branch + its `elif` case to `publish.yml`, push it onto that
51
- branch, and let it publish; (2) reference `@agilant/toga-blox@<channel>` in the app's build script.
52
+ Those channels are produced by the **`toga-blox-npm`** repo's now-fully-dynamic
53
+ `.github/workflows/publish.yml`, which publishes `@agilant/toga-blox@<mode>` for any pushed
54
+ `_<mode>` branch (branch → dist-tag, underscore stripped, 1:1). See the
55
+ [toga-blox dynamic publish pipeline](../../toga-blox/workflows/dynamic-publish-pipeline.md) for the
56
+ full cross-repo publish-then-build model. **The channel must exist before the app build runs.**
52
57
 
53
- ## Steps (to stand up a new environment, e.g. QC Security)
58
+ ## Steps (to stand up a new environment)
54
59
 
55
- 1. In `toga-blox-npm`: add the branch to `on.push.branches` and an `elif` case (version suffix +
56
- `DIST_TAG`) in `publish.yml`; create the publish branch **from** a branch that has the updated
57
- workflow (e.g. `_qc-security` from `_production`); push → it publishes `@agilant/toga-blox@qc-security`.
58
- Verify: `npm dist-tag ls @agilant/toga-blox`.
59
- 2. In `toga2-commerce`: add a `build<Env>` script (blox `@<channel>` install + `--legacy-peer-deps`
60
- + `tsc` + `vite build --mode <mode>`); add the `case` arm in `amplify.yml`; commit `.env.<mode>`.
61
- 3. Connect the Amplify branch; push; watch for the `Branch '...' -> Vite mode '...'` echo.
60
+ 1. In `toga-blox-npm`: create + push `_<mode>` → `publish.yml` publishes `@agilant/toga-blox@<mode>`
61
+ automatically. Verify: `npm view @agilant/toga-blox dist-tags`.
62
+ 2. In `toga2-commerce`: commit `.env.<mode>` (public base URLs only, no secrets).
63
+ 3. Connect the Amplify branch `_<mode>`; push; the inline spec derives the mode and builds.
62
64
 
63
65
  ## Systems involved
64
66
 
@@ -83,12 +85,22 @@ branch, and let it publish; (2) reference `@agilant/toga-blox@<channel>` in the
83
85
  peers: `@tanstack/react-table@^8`, `react-table@^7.8.0`, `react-table-sticky@^1.1.3`. Check
84
86
  `npm view @agilant/toga-blox@<tag> peerDependencies` against commerce deps when a resolve error
85
87
  appears.
86
- - **Secret hygiene:** commerce's committed `.npmrc` has historically held live FontAwesome + npm
87
- publish tokens. Rotate and untrack — keep tokens in env vars (`${NODE_AUTH_TOKEN}` /
88
- `${FONTAWESOME_NPM_TOKEN}`) as the blox `publish.yml` does.
88
+ - **Secret hygiene (open remediation):** `toga2-commerce/.npmrc` is **git-tracked** — it is listed
89
+ in `.gitignore` but was committed before that, so git still tracks it, and it holds a live
90
+ FontAwesome registry token and a live npm auth token in plaintext. Remediation: (1) **rotate both
91
+ tokens** (treat as compromised); (2) `git rm --cached .npmrc`; (3) move the tokens to Amplify
92
+ environment variables and write a **transient `.npmrc` at build time** from
93
+ `$FONTAWESOME_NPM_AUTH_TOKEN` — exactly what blox's `publish.yml` already does. Never document the
94
+ token values.
89
95
 
90
96
  ## Change history
91
97
 
98
+ - 2026-07-21 — Reworked `amplify.yml` to a **fully dynamic inline build** (no per-branch `case`, no
99
+ `npm run <script>`): derive mode from `$AWS_BRANCH`, dynamic `npm install
100
+ "@agilant/toga-blox@$MODE"`, `npx tsc -b` (composite), `npx vite build --mode "$MODE"`. Re-export
101
+ `NODE_OPTIONS=--max-old-space-size=8192` in the **build** phase (Rollup OOM). Publish chain now
102
+ dynamic (any `_<mode>` branch in toga-blox-npm). Flagged the git-tracked `.npmrc` live-token issue
103
+ with rotate/untrack/transient-`.npmrc` remediation. (jcardinal)
92
104
  - 2026-06-18 — Initial Amplify setup: added `amplify.yml` (8192 heap, underscore-strip, fail-loud,
93
105
  case→npm script), `.gitattributes` (LF), `sync-stage-environments.yml`; added `buildQcSecurity`
94
106
  with `@qc-security` blox channel + `--legacy-peer-deps` (and same flag on alpha/beta/gamma);
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
6
- | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
6
+ | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
7
7
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
@@ -6,9 +6,12 @@ project: TOGa Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-17
9
+ updated: 2026-07-21
10
10
  owners: [mhammontree]
11
11
  files:
12
+ - toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx
13
+ - toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx
14
+ - toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx
12
15
  - toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx
13
16
  - toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx
14
17
  - toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx
@@ -123,6 +126,10 @@ success/failure.
123
126
  `shipToAddressUuid` / `returnAddressUuid`.
124
127
  - **Decision:** a confirmation dialog was added even though the Pending-card trash deletes
125
128
  **without** one, because this is a large, explicit, destructive button.
129
+ - **Destructive-red confirm button (2026-07-21).** `SupplyFormGuardrail` defaults
130
+ `rightButtonClasses` to the primary blue (`#3F5DCA`). The delete-shipment confirm passes a
131
+ **crimson-700 (`#D60027`)** destructive style to signal the permanent-delete impact. Crimson-700 is
132
+ the app's destructive red; there is **no** crimson-800 token, so the hover uses a darker hex directly.
126
133
 
127
134
  ## Update Info / Edit Shipment form (config-driven; Figma-alignment pass 2026-07)
128
135
 
@@ -216,6 +223,22 @@ bare string** — see the gotcha below before wiring a new select field.
216
223
  12px check glyph. This is the **shared** checkbox, so the change applies **app-wide**, not just to
217
224
  the shipment forms — audit other consumers when touching it.
218
225
 
226
+ ### Mint-green "dirty field" border on the fulfill/ship forms (2026-07-21, Figma node 3554-5206)
227
+
228
+ A field changed from its saved value shows a **mint-600 (`#2CB224`) border**; it clears on save
229
+ (Update Info navigates away, the return modal closes). Implemented in `BaseInput.tsx` +
230
+ `renderEditShipmentFormInput.tsx`:
231
+ - **Opt-in only.** The mint styling is gated behind the existing **`hasDirtyValidation`** prop, so it
232
+ activates only on forms that pass it (enabled here via `renderEditShipmentFormInput`) — no app-wide
233
+ spread onto every `BaseInput`.
234
+ - **Dirty detection keys off `input.valueKey`.** `mint-600` = `#2CB224` in `tailwind.config.cjs`,
235
+ matching the Figma token.
236
+ - **⚠ `dirtyFields` lookup must use the field's real key.** `BaseInput`'s dirty check was looking up
237
+ `dirtyFields["undefined.message"]` (always `undefined`) for uuid-less fields, so **no** field ever
238
+ showed the mint border. Fix: key `isFieldDirty` off `input.valueKey`. Also — programmatic `setValue`
239
+ for a user-meaningful prefill (e.g. the warehouse-address prefill) must pass `{ shouldDirty: true }`
240
+ or RHF reports the field clean and the border never appears (see the RHF `isDirty` gotcha below).
241
+
219
242
  ## Signature Type — 4-level enum (2026-07)
220
243
 
221
244
  Signature moved from a **boolean `requiresSignature`** to a **4-level `signatureType` enum**:
@@ -472,6 +495,37 @@ the call 403s and returns no label.
472
495
  remove/relabel the nav link; (B) add a global "All Pending Shipments" view — drop the SO filter
473
496
  (client scoping is automatic from the JWT), but each row must then carry its own SO `internalId`
474
497
  for the edit/fulfill links.
498
+ - **⚠ `BasicTable` headers misalign from their column values unless you pass a HORIZONTAL
499
+ `headerCellPadding` (recurring).** `BasicTable` renders through daisyUI's `table` class, whose base
500
+ `:where(th,td)` applies ~16px horizontal cell padding. Body cells override it with
501
+ `cellPadding="px-2"` (8px), but headers by default override only the **vertical** padding — so header
502
+ text sits 8px off from the column values (and the select-all checkbox misaligns with the row
503
+ checkboxes). **Always pass a horizontal `headerCellPadding` (e.g. `"px-2 py-3"`) that matches
504
+ `cellPadding`.** Two related items fixed alongside: (1) a **sticky** header showed a doubled seam
505
+ before scroll (a real CSS `border` **plus** a box-shadow) — use a **box-shadow-only** seam for sticky
506
+ headers and add a left inset seam for the first column; column dividers `2px`→`1px` to match Figma;
507
+ (2) a cell that **double-pads** (e.g. a serial cell carrying its own `px-2`) must drop the extra
508
+ padding so it aligns at the same 8px gutter. Files: `components/ui/Tables/BasicTable/BasicTable.tsx`,
509
+ `ShipmentItems/view/forms/ShipmentItemsTable.tsx`, `EditShipment/view/components/SelectedShipmentItemsTable.tsx`.
510
+ - **⚠ React Hook Form `formState.isDirty` read ONLY inside an event handler is stale (`false`).**
511
+ `formState` is a **Proxy** that subscribes to a key only if you **read it during render**. Reading
512
+ `formState.isDirty` exclusively inside a handler (e.g. a modal close/backdrop handler) returns a
513
+ stale `false`, so a backdrop click closed the return modal without the save/discard guardrail firing.
514
+ **Fix: destructure/read `isDirty` during render** and use that value in the handler. Corollary: a
515
+ programmatic `setValue` for a user-meaningful prefill must pass `{ shouldDirty: true }` or the form
516
+ still reads "clean" (also affects the mint dirty-border above). File:
517
+ `EditShipment/view/modals/ReturnShippingModal.tsx`.
518
+ - **⚠ api2 V2 field SELECTORS are dot-paths of REAL DB column names — a wrong name silently returns
519
+ nothing.** V2 responses select fields by dot-path (e.g.
520
+ `"itemFulfillmentItems.salesOrderItem.item.skuNumber"`). The real `Items` columns are **`skuNumber`
521
+ / `upcCode`**; requesting `item.sku` / `item.upc` (which don't exist) returns **no value, no error**
522
+ — the field just comes back empty. (Manufacturer worked only because `manufacturer.name` is the
523
+ correct relation name.) When a field renders blank, verify the selector matches the actual DB column,
524
+ not a guessed alias, and keep the name consistent across the API request, the response mapping, the
525
+ component read, and the TS types. Concrete fix (SKU/UPC not displaying): standardized on
526
+ `skuNumber`/`upcCode` across `EditShipment/api/UpdateShipmentApi.ts`,
527
+ `view/components/forms/EditShipmentForm.tsx`, `SelectedShipmentItemsTable.tsx`, `types.ts`, and
528
+ `ShipmentItems/view/forms/ShipmentItemsTable.tsx` + `types.ts`.
475
529
 
476
530
  ## Remaining work
477
531
 
@@ -534,6 +588,16 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
534
588
  for Compass and is not yet in prod.
535
589
 
536
590
  ## Change history
591
+ - 2026-07-21 — TRUE-79191 (toga2-supply UI fixes + Figma polish): documented three durable, reusable
592
+ gotchas — (1) **`BasicTable` header/value misalignment** (daisyUI base `:where(th,td)` ~16px padding;
593
+ always pass a horizontal `headerCellPadding` matching `cellPadding`, box-shadow-only seam for sticky
594
+ headers, drop double-padding on self-padded cells); (2) **RHF `formState.isDirty` is stale when read
595
+ only inside an event handler** (Proxy subscribes on render-time reads only) — read it during render;
596
+ `setValue` prefills need `{ shouldDirty: true }`; (3) **api2 V2 field selectors are dot-paths of real
597
+ DB column names** — `item.sku`/`item.upc` silently returned nothing; the real columns are
598
+ `skuNumber`/`upcCode`. Built the **mint-600 (#2CB224) dirty-field border** (opt-in via
599
+ `hasDirtyValidation`; fixed the `dirtyFields["undefined.message"]` bug by keying off `input.valueKey`)
600
+ and gave the **Delete Shipment** confirm a crimson-700 (#D60027) destructive button. (mhammontree)
537
601
  - 2026-07-17 — TRUE-79191 (David design-review pass, toga2-supply UI): documented four durable,
538
602
  app-wide component gotchas — (1) **`BaseInput` react-select menus now portal to `<body>`**
539
603
  (`menuPortalTarget` + `menuPortal zIndex 9999`) so a sibling scroll box can't clip them, and the
@@ -5,10 +5,11 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-07-17
8
+ updated: 2026-07-21
9
9
  owners: [jcardinal]
10
10
  related:
11
11
  - ../apps/toga25-supply/workflows/amplify-deployment.md
12
+ - ../apps/toga-blox/workflows/dynamic-publish-pipeline.md
12
13
  ---
13
14
 
14
15
  ## Scope
@@ -34,3 +35,25 @@ Applies to all 2.0 Vite + React/TS frontends deployed on AWS Amplify.
34
35
 
35
36
  ## Node
36
37
  - Vite 7 requires Node 20.19+ or 22.12+ (Node 22 recommended). Pin via `.nvmrc` if needed.
38
+
39
+ ## Per-environment library channel (optional; when an app installs a versioned shared lib)
40
+
41
+ Some 2.0 frontends install a **per-environment channel** of a shared npm library instead of a
42
+ single pinned version (e.g. `toga2-commerce` installs `@agilant/toga-blox@$MODE`, channel == mode,
43
+ strict 1:1). When an app does this:
44
+
45
+ - The library's own release repo must publish the channel **before** the app build runs — a
46
+ `_<mode>` branch in the *app* repo only tells Amplify what to build; it does not create a
47
+ channel. Install-by-channel fails `ETARGET No matching version` until the library has published.
48
+ See `../apps/toga-blox/workflows/dynamic-publish-pipeline.md`.
49
+ - Install the channel dynamically from the derived mode (`npm install "@lib@$MODE"`), not from a
50
+ hardcoded per-branch list, so one `amplify.yml` serves every environment.
51
+ - `--legacy-peer-deps` may be required when the app's and library's peer ranges diverge; when it
52
+ is used, npm will not auto-install the library's peers, so any peer not already in the app's
53
+ dependencies must be added explicitly.
54
+
55
+ ## Build memory / heap
56
+
57
+ For memory-heavy Rollup builds, set `NODE_OPTIONS=--max-old-space-size=<MB>` and **re-export it in
58
+ the `build` phase**, not only `preBuild` — Amplify may use a separate shell per phase, so a
59
+ preBuild-only export leaves the Rollup step on the default heap and it OOMs.
@@ -30,7 +30,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
30
30
  - **ai-bdr** (AI-BDR) — 8 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
31
31
  - **toga2-commerce** (TOGa Commerce) — 9 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
32
32
  - **toga25-supply** (TOGa 2.5 Supply) — 10 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
33
- - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
33
+ - **toga-blox** (TOGa Blox) — 8 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
34
34
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
35
35
 
36
36
  ## standalone framework
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.389",
3
+ "version": "1.0.391",
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",