toga-ai 1.0.665 → 1.0.666

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,6 +7,7 @@
7
7
  | [AdvancedSelect (virtualized single/multi select)](features/advanced-select.md) | `AdvancedSelect<T>` is a **fully controlled, virtualized** single/multi select built from scratch (not react-select) on **`@tanstack/react-virtual`**, for large | toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx, toga-blox/src/components/AdvancedSelect/AdvancedSelect.types.ts, toga-blox/src/components/AdvancedSelect/AdvancedSelect.module.css |
8
8
  | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
9
9
  | [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
10
+ | [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | The standing list of **defects to fix inside `@agilant/toga-blox`**. | toga-blox/src/api/auth.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/components/BaseInput, toga-blox/tsconfig.json, toga-blox/package.json |
10
11
  | [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/components/Table/themeConfig/toga.module.css, 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 |
11
12
  | [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 |
12
13
  | [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/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, 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 |
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-08-27
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
@@ -20,6 +20,7 @@ related:
20
20
  - features/base-input.md
21
21
  - features/advanced-select.md
22
22
  - features/api-client.md
23
+ - features/blox-authoring-defects.md
23
24
  ---
24
25
 
25
26
  ## Summary
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-17
9
+ updated: 2026-08-27
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/reactQuery/queryHelpers.ts
@@ -118,8 +118,48 @@ Filterable`, `hyperlinkField`, `imageUrlField`, `sticky`, `precision`, …), `Da
118
118
  paren pair so the group re-wrap doesn't double-wrap. Any raw pre-formatted where clause was
119
119
  100% broken before this. If you add a new raw-string clause path, route it through this string
120
120
  branch — do **not** feed a raw clause into an object-expecting serializer.
121
+ - **⚠ AUTHORING DEFECT — `handleClientAuthentication(uuid)` collides by name with app-local SSO
122
+ helpers and is not interchangeable with them.** blox's version `POST`s
123
+ `/auth/encrypted-user-uuid` with an **empty payload** (`{client:"",user:""}`), writes
124
+ access/refresh tokens to `localStorage`, and returns a `/users` payload. `toga2-supply` has a
125
+ local util of the **same name** that resolves a client's SSO URL — a completely different
126
+ function. `toga25-supply` called blox's by mistake and SSO silently never fired for ~any data,
127
+ because the returned object has no `singleSignOnServiceUrl` property (masked by an `as any`).
128
+ See [toga25-supply SSO redirect & session gating](../../toga25-supply/features/sso-redirect-and-session-gating.md).
129
+ - **⚠ AUTHORING DEFECT — `fetchPublicToken()` PERSISTS the public token to `localStorage`**
130
+ (`accessToken` + `refreshToken`), so a logged-**out** visitor is indistinguishable from a
131
+ logged-in one if you gate on `accessToken`. The correct discriminator is `localStorage`'s
132
+ **`user`** key — which this module's own request interceptor already uses
133
+ (`userString ? accessToken : fetchPublicToken(baseURL)`). Corollary: `localStorage` repopulating
134
+ seconds after logout is a **fresh public token**, not a surviving session, and no app-side code
135
+ can stop it while this behavior stands.
136
+ - **⚠ AUTHORING DEFECT — `performLogout` hardcodes `window.location.href = "/"`.** On a
137
+ single-sign-on client, `/` is exactly the route that triggers the IdP redirect, so logging out
138
+ bounces the user straight back into SSO and `/login` becomes unreachable. Apps can pass
139
+ `onLogout` to `createAxiosInstance` **and** stop calling `performLogout` from their own logout
140
+ action — **both**, since the Logout button and the 401-interceptor path are separate — but the
141
+ real fix is blox-authoring work.
142
+ - **`GET /domains` requires a Bearer token.** The request interceptor transparently supplies a
143
+ *public* one when there is no `user` in `localStorage`, which is why a logged-out domain lookup
144
+ works in-app but **401s from a raw `curl`**. Reproduce out-of-app by minting a public token first.
145
+ - **Verifying a domain is registered without DB access:** `POST /auth/public` with an explicit
146
+ `Origin` header. `Origin: https://compass.togasupply.com` → **201** with an audience of
147
+ `{client: "Compass Group", app: "TOGa Supply"}`; no `Origin` at all → **401 EN-6**.
148
+ - **OPEN QUESTION (unresolved 2026-08-27): `EV-14` from a filtered `/domains` lookup.** `EV-14` is a
149
+ **404 "expected exactly one record"**. A public-token `/domains` lookup returned it for every
150
+ environment slug tried (`PRODUCTION`, `PROD`, `LIVE`). It was **not** determined whether the
151
+ app's own in-browser request differs (different token scope, different filter). Treat as an open
152
+ thread, not a settled finding.
121
153
 
122
154
  ## Change history
155
+ - 2026-08-27 — Recorded three blox **authoring defects** surfaced while debugging toga25-supply SSO:
156
+ (1) `handleClientAuthentication` collides by name with app-local SSO helpers and returns a
157
+ `/users` payload with no `singleSignOnServiceUrl`; (2) `fetchPublicToken` persists the public
158
+ token to `localStorage`, making logged-out visitors look authenticated unless you gate on the
159
+ `user` key; (3) `performLogout`'s hardcoded `"/"` redirect traps SSO clients in a logout→IdP loop
160
+ and makes `/login` unreachable. Also documented that `/domains` needs a Bearer token (public one
161
+ auto-supplied in-app, hence raw-curl 401s), the `Origin`-header trick for confirming a domain is
162
+ registered, and left the public-token `/domains` **EV-14** result as an open question. (apeterson)
123
163
  - 2026-08-17 — Fixed `assembleOptionsWhere` spreading a raw-string `apiWhereClause` character-by-
124
164
  character into invalid `index:0:char` conditions (api2 `Invalid operator '0'` / EO-1 500 opening
125
165
  the Compass USA Item Fulfillment modal). String entries now pass through verbatim (single outer
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: blox authoring defect backlog (fix IN blox, not around it)
3
+ framework: "2.0"
4
+ repo: toga-blox
5
+ project: TOGa Blox
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-27
10
+ owners: [apeterson]
11
+ files:
12
+ - toga-blox/src/api/auth.ts
13
+ - toga-blox/src/api/axiosInstance.ts
14
+ - toga-blox/src/reactQuery/queryHelpers.ts
15
+ - toga-blox/src/components/BaseInput
16
+ - toga-blox/tsconfig.json
17
+ - toga-blox/package.json
18
+ related:
19
+ - ../architecture.md
20
+ - api-client.md
21
+ - base-input.md
22
+ - advanced-select.md
23
+ - ../../../standards/frontend.md
24
+ - ../../toga25-supply/features/sso-redirect-and-session-gating.md
25
+ ---
26
+
27
+ ## What it is
28
+
29
+ The standing list of **defects to fix inside `@agilant/toga-blox`**. This is the counterpart to
30
+ [frontend standard §22](../../../standards/frontend.md), which tells a *consuming app* how to work
31
+ **around** the same defects. §22 is defensive advice for app authors; **this doc is the work list
32
+ for whoever edits blox itself**, and it is where §22's long-standing "a future `blox-authoring.md`"
33
+ pointer resolves.
34
+
35
+ Nothing here is scheduled. Treat it as a backlog with evidence attached, so a fix can be picked up
36
+ without re-deriving the mechanism. **When one of these is fixed and published, update §22 in the
37
+ same pass** — otherwise consuming apps keep carrying dead workarounds.
38
+
39
+ ## How it works (how to use this list)
40
+
41
+ Each entry carries three things deliberately: the **mechanism** (what the code actually does), the
42
+ **consumer-facing symptom** (how it presents in an app, which is usually nothing like the
43
+ mechanism), and a **fix direction** (a suggestion, not a decision). Every one of these is a
44
+ **breaking-ish change for ~40 consumers**, so a fix needs a deprecation path or a coordinated bump
45
+ — see the [publish pipeline](../workflows/dynamic-publish-pipeline.md).
46
+
47
+ ## Auth-layer defects (found 2026-08-27, toga25-supply SSO debugging)
48
+
49
+ ### 1. `handleClientAuthentication` — misleading name, untyped return
50
+
51
+ - **Mechanism.** Despite the name, it is not a client-authentication *flow driver*: it is an
52
+ **encrypted-uuid token exchange**. It `POST`s `/auth/encrypted-user-uuid` with an empty payload
53
+ (`{client:"",user:""}`), writes access/refresh tokens to `localStorage`, and returns a `/users`
54
+ payload. Its return type is not modeled, so consumers reach into it untyped.
55
+ - **Consumer symptom.** `toga2-supply` has an app-local util of the **exact same name** that
56
+ resolves a client's SSO URL. `toga25-supply` called blox's by mistake; the returned object has no
57
+ `singleSignOnServiceUrl` property, so the SSO redirect evaluated to `null` under **any** data and
58
+ silently fell through to `/login` forever. An `as any` cast is what let it compile and ship.
59
+ - **Fix direction.** **Rename it to describe what it does** (e.g. an `exchangeEncryptedUserUuid`
60
+ shape) so it cannot collide with app-level SSO helpers, and **declare a return type** so a shape
61
+ mismatch is a compile error at the call site instead of a silent `undefined`.
62
+
63
+ ### 2. `fetchPublicToken` — persists the public token to `localStorage`
64
+
65
+ - **Mechanism.** It writes both `accessToken` and `refreshToken` to `localStorage` for
66
+ **logged-out** requests, using the same keys a real session uses.
67
+ - **Consumer symptom.** A public token becomes **indistinguishable from a user session** for any
68
+ consumer that gates on `accessToken` — which is the obvious thing to gate on. Bugs present as
69
+ intermittent and environment-dependent (clean storage behaves one way, a browser that has already
70
+ touched the API behaves another). It also makes `localStorage` repopulate seconds after logout,
71
+ which reads as "logout is broken" when it is a fresh public token. The only current discriminator
72
+ is the `user` key — which blox's own request interceptor already relies on
73
+ (`userString ? accessToken : fetchPublicToken(baseURL)`), so the library is internally aware of a
74
+ distinction it does not expose.
75
+ - **Fix direction.** **Hold the public token in memory** (module scope) rather than persisting it,
76
+ so `localStorage` means "a real session exists" and nothing else. If persistence is genuinely
77
+ needed for reloads, use a **separate key** (`publicAccessToken`) so the two can never be confused.
78
+
79
+ ### 3. `performLogout` — hardcoded `window.location.href = "/"`
80
+
81
+ - **Mechanism.** The logout destination is a literal `"/"`, not configurable by the caller.
82
+ - **Consumer symptom.** On any **SSO client**, `/` is precisely the route that triggers the IdP
83
+ redirect — so signing out bounces the user straight back into SSO and the local `/login` becomes
84
+ **unreachable**. Worse, an app cannot fully patch around it from one place: the Logout button goes
85
+ through the app's own auth context while an expired token goes through blox's 401 interceptor, so
86
+ a workaround must cover **both** paths. `toga25-supply` built that workaround and then reverted
87
+ it, so the loop is live there today.
88
+ - **Fix direction.** Make the destination **caller-supplied** (a `redirectTo` option, defaulting to
89
+ today's `"/"` for compatibility), or have `performLogout` simply not navigate and let the caller's
90
+ `onLogout` own it.
91
+
92
+ ## Already-known authoring items (carried over from §22)
93
+
94
+ These are long-standing and were previously recorded only as consumer-side advice:
95
+
96
+ - **Empty `where` serializes to a literal `()`** in `reactQuery/queryHelpers.ts` — apps must avoid
97
+ emitting an empty filter object. Fix direction: omit the clause entirely when it has no entries.
98
+ - **`dist/main.css` is not compiled** — it still contains raw `@tailwind` directives, so importing
99
+ it does nothing useful. Fix direction: either build it properly or stop shipping it, because its
100
+ presence invites consumers to import it.
101
+ - **Two select stacks** (`react-select` + the custom `AdvancedSelect`) and **two table stacks**
102
+ (`react-table` v7 + `@tanstack/react-table` v8) coexist. This is real dependency weight pushed
103
+ onto every consumer, and the v7 stack is what forces apps to declare `react-table` /
104
+ `react-table-sticky` / `react-multi-select-component` peers. Fix direction: converge on one of
105
+ each and retire the legacy stack.
106
+ - **`BaseInput` has a ~85-prop className-string styling contract** and **no custom-validator
107
+ passthrough** — the latter is why every consuming app rebuilds an app-side validator layer. Fix
108
+ directions: drive styling from host tokens rather than per-prop strings, and accept a validator
109
+ passthrough.
110
+ - **`strict: false` in blox's own tsconfig.** The library ships looser types than the apps that
111
+ consume it, which is how untyped returns like defect 1 survive review. Fix direction: enable
112
+ strict incrementally.
113
+ - **React-18-only peers.** Blocks every consumer from React 19; unscheduled and consumer-wide.
114
+
115
+ ## Gotchas
116
+
117
+ - **Do not "fix" these in an app.** Working around them in a consumer is §22's job and is
118
+ legitimate; changing app code to *imitate* the defect is not.
119
+ - **A fix here is a fleet-wide change.** blox is unbundled and consumed by ~40 call sites; anything
120
+ touching auth or `localStorage` keys changes live session behavior on the next channel head that
121
+ a consumer picks up — and `toga2-commerce` picks up the channel head **automatically at build
122
+ time**, with no PR. Sequence auth fixes deliberately.
123
+
124
+ ## Change history
125
+ - 2026-08-27 — Created the doc §22 had been pointing at since 2026-08-18. Seeded with the three auth
126
+ defects found while debugging toga25-supply SSO (`handleClientAuthentication`'s misleading name +
127
+ untyped return, `fetchPublicToken` persisting the public token to `localStorage`,
128
+ `performLogout`'s hardcoded `"/"`), each with mechanism / consumer symptom / fix direction, plus
129
+ the previously consumer-side-only items carried over from §22. (apeterson)
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-26
9
+ updated: 2026-08-27
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga-blox/.github/workflows/publish.yml
@@ -167,8 +167,39 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
167
167
  - **Secret hygiene done right.** `publish.yml` writes a transient `.npmrc` at build time from
168
168
  GitHub secrets (`${NODE_AUTH_TOKEN}` / FontAwesome token) — it never commits tokens. This is
169
169
  the pattern the consuming apps should follow (see the commerce workflow's secret-hygiene note).
170
+ - **⚠ Sibling to the `.scss`-only gotcha, DIFFERENT mechanism: a stylesheet whose import path
171
+ ESCAPES `dist/` is never in the tarball.** After the `feature-new-table` → `_production` merge,
172
+ `global.css` sat at the **repo root** with `import "../global.css"` in `src/index.ts`. `tsc`
173
+ emits that specifier **verbatim**, so `dist/index.js` pointed *outside* `dist/`, and
174
+ `"files": ["dist","README.md"]` meant the stylesheet was never packed. Every consumer failed with
175
+ `Could not resolve "../global.css"`. Shipped broken as **1.0.323-production.137** and
176
+ **1.1.1-production.138**. Fix: move it to `src/global.css` with `import "./global.css"` so
177
+ `copyfiles` ships it into `dist/` — which is exactly what `_sandbox-client` had always published
178
+ (identical blob, different location). **This was a merge-resolution failure, not new code** —
179
+ when landing a long-lived branch, diff the *published tarballs* of the two channels, not just the
180
+ source. Same failure family as the `.scss` gotcha above (JS importing an unshipped stylesheet),
181
+ but caused by the path + the `files` allowlist rather than the copy glob.
182
+ - **There is no base version that publishes as itself.** CI always does **patch + 1** on the base,
183
+ so a base of `1.1.0` can never publish as `1.1.0` — it publishes `1.1.1-production.<run>`. The
184
+ developer controls only the base; plan release numbering around that.
185
+ - **Keep the production base ahead of, or level with, sandbox.** `_production`'s base had drifted
186
+ **12 patches behind** `_sandbox-client`, so the production channel was publishing *numerically
187
+ lower* versions than sandbox. The base was bumped `1.0.322` → **1.1.0** to mark the
188
+ `feature-new-table` release and stop that inversion; CI published `1.1.1-production.139`.
189
+ - **Pushing to a `_*` branch IS the publish — there is no separate step to run afterwards.** The
190
+ merge push itself triggered a publish (`1.0.323-production.137`) about a minute after the merge
191
+ landed. Consequence: a merge that is not release-ready still ships. Get the tree right *before*
192
+ you push to a release branch.
170
193
 
171
194
  ## Change history
195
+ - 2026-08-27 — Released blox **1.1.0** on `_production` (published `1.1.1-production.139`) and fixed
196
+ a shipped-broken tarball: the `feature-new-table` → `_production` merge left `global.css` at the
197
+ repo root with `import "../global.css"`, so the emitted `dist/index.js` referenced a path outside
198
+ `dist/` that `"files": ["dist"]` never packed — breaking every consumer on
199
+ `1.0.323-production.137` / `1.1.1-production.138`. Moved to `src/global.css` + `./global.css`.
200
+ Also recorded that CI's patch+1 means no base ever publishes as itself, that the prod base had
201
+ fallen 12 patches behind sandbox (hence the `1.1.0` bump), and that pushing a `_*` branch **is**
202
+ the publish. (apeterson)
172
203
  - 2026-08-26 — Deployed `1.0.334-sandbox-client.136` (`feature-new-table` → `_sandbox-client`, 6
173
204
  files, no conflicts) and recorded three workflow gotchas from it: (1) a feature branch that never
174
205
  touches the `version` line merges without a version conflict, which is why the bump lands on
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-26
9
+ updated: 2026-08-27
10
10
  owners: ["jcardinal", "apeterson"]
11
11
  files:
12
12
  - toga2-commerce/amplify.yml
@@ -129,8 +129,28 @@ full cross-repo publish-then-build model. **The channel must exist before the ap
129
129
  environment variables and write a **transient `.npmrc` at build time** from
130
130
  `$FONTAWESOME_NPM_AUTH_TOKEN` — exactly what blox's `publish.yml` already does. Never document the
131
131
  token values.
132
+ - **⚠ The `package.json` blox pin governs LOCAL DEV ONLY — the deployed build always takes the
133
+ channel head.** `amplify.yml` runs `npm install "@agilant/toga-blox@$MODE"`, which **overwrites**
134
+ whatever version `package.json` pins. So a production deploy silently picks up a newly published
135
+ blox release **with no PR and no pin change at all** — and conversely, "bump the pin" accomplishes
136
+ much less here than it does in `toga25-supply`, which installs blox as a normal pinned dependency.
137
+ Reason about commerce's deployed blox version from the **dist-tag**, not the manifest.
138
+ - **A caret on a PRERELEASE is inert, not floating.** `^1.0.322-sandbox-client.117` resolves to
139
+ **exactly** `1.0.322-sandbox-client.117`: npm only matches prereleases sharing the same
140
+ `major.minor.patch`. Verified with `semver.maxSatisfying`. It is still a
141
+ [frontend standard](../../../standards/frontend.md) §13(a) violation and confusing, but it is
142
+ **not** a drift risk — don't chase it as one.
143
+ - **Commerce's dependency health is better than supply's**, verified 2026-08-27: all 19 blox peers
144
+ satisfied (none missing) and `resolve.dedupe` is committed on `TRUE-80707`.
132
145
 
133
146
  ## Change history
147
+ - 2026-08-27 — Replaced the caret sandbox blox pin `^1.0.322-sandbox-client.117` with the exact
148
+ production-channel `1.1.1-production.139` per frontend standard §13(a), verified with the same
149
+ chain Amplify runs (`tsc -b` + `vite build --mode production`, green). **Working-tree only /
150
+ uncommitted on `TRUE-80707`.** Recorded two findings: `amplify.yml`'s
151
+ `npm install "@agilant/toga-blox@$MODE"` **overwrites** the manifest pin, so the pin governs local
152
+ dev only and a prod deploy takes the channel head with no PR; and a caret on a prerelease is
153
+ **inert**, resolving to that exact version rather than floating. (apeterson)
134
154
 
135
155
  - 2026-08-26 — Dependency-resolution findings from a local commerce build: root-caused an
136
156
  `ERESOLVE` to the **missing `legacy-peer-deps=true` in commerce's tracked `.npmrc`** (supply had
@@ -10,6 +10,7 @@
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
12
12
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
13
+ | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
13
14
  | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
14
15
  | [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
15
16
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: SSO redirect & public-vs-user session gating (useAuthenticationFlow)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-27
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/hooks/useAuthenticationFlow.ts
13
+ - toga25-supply/src/routes.tsx
14
+ - toga25-supply/src/contexts/AuthContext.tsx
15
+ - toga25-supply/src/api/api.ts
16
+ related:
17
+ - ../../toga-blox/features/api-client.md
18
+ - ../../api2/features/encrypted-user-uuid-auth-handoff.md
19
+ - ../../../standards/frontend.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to
25
+ their client's SSO IdP instead of the local `/login` form. `useAuthenticationFlow` resolves the
26
+ client from the current domain, asks api2 for that client's `singleSignOnServiceUrl`, and
27
+ redirects there if one exists. This mirrors `toga2-supply`, which invokes the equivalent flow from
28
+ `App.tsx` on every navigation.
29
+
30
+ Confirmed working in production for Compass USA / Compass Canada as of 2026-08-27.
31
+
32
+ ## How it works
33
+
34
+ 1. **The hook must be mounted inside the router.** It uses `useNavigate` / `useLocation`, so it is
35
+ mounted as an `AuthenticationFlowRoot` **route element** that wraps *both* the `PrivateRoute`
36
+ branch and `/login` in `routes.tsx`. Mounting it above the router, or not at all, silently
37
+ disables SSO.
38
+ 2. **Resolve the SSO URL from the scripted API, not from a login helper.** The correct call is
39
+ `GET /client-authentications/singleSignOnServiceUrl` with `{ uuid, domainUuid }`, reading
40
+ `data.clientAuthentications.singleSignOnServiceUrl`. A typed `SsoUrlResponse` interface holds
41
+ that shape so a mismatch is a compile error.
42
+ 3. **Redirect or fall through.** A non-null URL → `window.location` to the IdP; null → local
43
+ `/login`.
44
+ 4. **Gate on a real user session, not on a token.** The "is this visitor already signed in?" check
45
+ reads `localStorage.getItem("user")` — **not** `accessToken` (see gotchas).
46
+
47
+ ## Gotchas
48
+
49
+ - **⚠ blox exports a `handleClientAuthentication(uuid)` that is NOT `toga2-supply`'s util of the
50
+ same name.** This is a **name collision between a shared-lib export and an app-local helper**,
51
+ and it was the root cause of SSO never firing here. blox's version `POST`s
52
+ `/auth/encrypted-user-uuid` with an **empty payload** (`{client:"",user:""}`), writes
53
+ access/refresh tokens to `localStorage`, and returns a `/users` payload that has **no
54
+ `singleSignOnServiceUrl` property at all**. The old code did
55
+ `(ssoData as any)?.singleSignOnServiceUrl ?? null`, so the result was **always null under any
56
+ data** and always fell through to `/login`. The `as any` cast is precisely what let this ship —
57
+ when consuming a shared-lib function whose name matches a local one, type the response and
58
+ verify the endpoint it actually calls.
59
+ - **⚠ A token in `localStorage` does not mean a user is signed in.** blox's `fetchPublicToken`
60
+ writes `accessToken` **and** `refreshToken` for *logged-out* requests. Gating on
61
+ `localStorage.getItem("accessToken")` (which both `hasTokenInStorage` in the hook and
62
+ `AuthContext`'s initial `isAuthenticated` did) makes any visitor who has already touched the API
63
+ look authenticated, so SSO is **skipped**. Symptom is maddeningly intermittent: clean storage →
64
+ SSO fires; any prior request → no SSO. **`localStorage.getItem("user")` is the correct
65
+ discriminator** — only a real session writes `"user"`, and blox's own axios request interceptor
66
+ already uses exactly that distinction (`userString ? accessToken : fetchPublicToken(baseURL)`).
67
+ *(Status: fix written but **uncommitted** as of 2026-08-27.)*
68
+ - **Tokens reappearing right after logout are not a surviving session.** They are a **fresh public
69
+ token** minted by `fetchPublicToken` on the next request. No app-side logout code can prevent
70
+ this while blox persists public tokens to `localStorage`.
71
+ - **⚠ Known live defect: `/login` is currently unreachable on an SSO client.** blox's
72
+ `performLogout` hardcodes `window.location.href = "/"`, and `/` is exactly what triggers the SSO
73
+ redirect — so signing out bounces straight back to the IdP. A fix landing on `/login` from both
74
+ paths (`AuthContext.logout` no longer calling `performLogout`, **plus** an `onLogout` passed
75
+ through `api.ts` so blox's 401-interceptor path matches — **both** are required, since the Logout
76
+ button goes through `AuthContext` and an expired token goes through the interceptor) was made in
77
+ `e886afb` and then **reverted at the developer's request** in `09f8dd5` (a revert, not a history
78
+ rewrite, because `e886afb` was already pushed and merged). The revert was **not pushed** as of
79
+ 2026-08-27. The underlying defect is blox's hardcoded `"/"`, and the app-side workaround was
80
+ rejected — so this is **blox-authoring work**.
81
+ - **A `?local=1` sessionStorage opt-out is unnecessary — don't rebuild it.** It was built and
82
+ discarded: once you submit the login form you hold a real token, so SSO stops firing on its own.
83
+
84
+ ### Diagnosing whether a hook is even running: grep the deployed bundle
85
+
86
+ Search the built bundle for the endpoint **string literals** the code should contain. Before the
87
+ fix: `client-authentications` 0 occurrences, `encrypted-user-uuid` 1. After: inverted. Because an
88
+ unmounted hook is tree-shaken out entirely, the absence of its literals also proves it is never
89
+ mounted — a cheap way to confirm a deploy actually carries the change.
90
+
91
+ ## Change history
92
+ - 2026-08-27 — Fixed SSO never redirecting: mounted the previously-callerless
93
+ `useAuthenticationFlow` as an in-router `AuthenticationFlowRoot`, and replaced the call to blox's
94
+ same-named `handleClientAuthentication` (empty-payload `/auth/encrypted-user-uuid`, no
95
+ `singleSignOnServiceUrl` in its response) with the scripted
96
+ `GET /client-authentications/singleSignOnServiceUrl`, typed via `SsoUrlResponse` instead of
97
+ `as any`. Committed, pushed, confirmed working. Also recorded (fix **uncommitted**) that public
98
+ tokens are indistinguishable from user tokens when gating on `accessToken` — gate on `"user"` —
99
+ and that the logout→`/`→SSO loop, briefly fixed in `e886afb`, was **reverted** (`09f8dd5`,
100
+ unpushed) so `/login` is currently unreachable on Compass. (apeterson)
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-17
10
- owners: [jcardinal]
9
+ updated: 2026-08-27
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/amplify.yml
13
13
  - toga25-supply/src/api/api.ts
@@ -111,8 +111,32 @@ and are the single source of truth for environment API endpoints.
111
111
  build is required. The `.env.<mode>` files are the single source of truth.
112
112
  - **`npm run build` ships production everywhere** — always build with an explicit
113
113
  `--mode "$VITE_MODE"`; the per-mode `package.json` scripts are dev servers, not builds.
114
+ - **⚠ A symlinked shared-lib checkout MASKS both a missing dependency declaration and missing peer
115
+ deps.** Verified 2026-08-27: `_production` did **not** list `@agilant/toga-blox` in
116
+ `package.json` at all and had **no lock entry**, despite ~200 imports across `src/`. It only
117
+ resolved locally because `node_modules/@agilant/toga-blox` was a **symlink to the local checkout**
118
+ (`toga-blox-npm`), so resolution fell through to the linked package's own `node_modules`. The
119
+ Amplify `preBuild` runs **`npm ci`**, which would have failed outright. Installing blox properly
120
+ from the registry then surfaced **four** peers the symlink had been hiding — because
121
+ `legacy-peer-deps=true` means npm does **not** auto-install peers, and blox's barrel import pulls
122
+ in components the app never renders:
123
+ `@fortawesome/free-solid-svg-icons@^6.7.2` (blox `Input.tsx` / `faCircleInfo`),
124
+ `react-multi-select-component@^4.3.4`, `react-table@^7.8.0`, `react-table-sticky@^1.1.3`
125
+ (the legacy v7 table stack). All three `react-table` v7 packages were **already** declared at
126
+ identical versions on `_sandbox-client` — `_production` was the outlier. Fixed in `92644c8`
127
+ (pushed), verified with a clean vite build (1761 modules).
128
+ - **The audit that catches the above — reusable for any blox consumer.** Enumerate the shared lib's
129
+ `peerDependencies` from `node_modules/<pkg>/package.json`, then check each one for a **real
130
+ directory** (not a symlink passthrough) in the *app's* `node_modules`. Do this whenever a repo
131
+ has ever been developed against a linked local checkout.
114
132
 
115
133
  ## Change history
134
+ - 2026-08-27 — Fixed `_production` never declaring `@agilant/toga-blox` (no dependency entry, no
135
+ lock entry, ~200 imports) — it resolved only via a local **symlink**, and `npm ci` in `preBuild`
136
+ would have failed. Added blox plus the four peers the symlink had masked
137
+ (`@fortawesome/free-solid-svg-icons`, `react-multi-select-component`, `react-table`,
138
+ `react-table-sticky`), which `_sandbox-client` already carried. Committed `92644c8`, pushed.
139
+ Recorded the reusable peer-audit procedure. (apeterson)
116
140
  - 2026-07-17 — Documented the single shared `amplify.yml` multi-environment deploy: `_<mode>`
117
141
  branch → `--mode` derivation, fail-loud guardrails, runtime `import.meta.env.MODE` identity,
118
142
  and the critical never-set-`VITE_API`-in-the-Amplify-console gotcha (console vars override
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-08-26
8
+ updated: 2026-08-27
9
9
  owners: [jcardinal, apeterson]
10
10
  files: []
11
11
  related:
@@ -422,6 +422,15 @@ on the back end. A consuming app **MUST** meet every requirement below.
422
422
  Verified 2026-08-26: `toga2-commerce` violates this with `"^1.0.322-sandbox-client.117"`. Also audit
423
423
  for **pin drift** between branches (commerce's `TRUE-80707` was 11 versions behind `_sandbox-client`).
424
424
 
425
+ **Nuance (verified 2026-08-27): a caret on a PRERELEASE is inert, not floating.** npm only matches
426
+ prereleases sharing the same `major.minor.patch`, so `^1.0.322-sandbox-client.117` resolves to
427
+ **exactly** `1.0.322-sandbox-client.117` (confirmed with `semver.maxSatisfying`). It is still a
428
+ violation — it misleads every reader about intent — but do **not** justify it as a drift risk. The
429
+ real floating risk lives elsewhere: `toga2-commerce`'s `amplify.yml` runs
430
+ `npm install "@agilant/toga-blox@$MODE"`, which **overwrites the manifest pin at build time**, so
431
+ commerce's deployed build always takes the channel head and its pin governs local dev only.
432
+ Commerce's caret is now the exact `1.1.1-production.139` (working-tree only on `TRUE-80707`).
433
+
425
434
  **(b) Declare blox's peer deps that do not hoist into your app.** blox lists them as peers, so npm
426
435
  will not necessarily install them for you: verified 2026-08-18 they include `react-hook-form`,
427
436
  `@tanstack/react-query`, `@tanstack/react-table`, `framer-motion`, and `axios` (pinned exactly by
@@ -597,9 +606,24 @@ Work **around** these shared-lib defects; do **not** imitate them in app code:
597
606
  - A **~85-prop className-string styling contract on `BaseInput`** — verbose by design; drive it from
598
607
  tokens, not per-instance strings.
599
608
  - **`strict: false` in blox's own tsconfig**, and **React-18-only** peers.
600
-
601
- **Fixing these is blox-authoring work, tracked separately** (a future `blox-authoring.md`), **not
602
- app work.** In a consuming app, guard against them and move on.
609
+ - **⚠ `handleClientAuthentication(uuid)` collides by name with app-local SSO helpers.** blox's
610
+ version POSTs `/auth/encrypted-user-uuid` with an empty payload and returns a `/users` payload
611
+ with **no `singleSignOnServiceUrl`** — `toga2-supply` has an unrelated util of the same name.
612
+ Cost `toga25-supply` a silently-dead SSO redirect, masked by an `as any`. **Type shared-lib
613
+ responses; never `as any` a cross-package result.**
614
+ - **⚠ `fetchPublicToken` persists the public token to `localStorage`**, so gating auth on
615
+ `accessToken` makes logged-out visitors look signed in (intermittent-looking bugs: clean storage
616
+ works, a warm one doesn't). Gate on the **`user`** key — blox's own interceptor already does.
617
+ - **⚠ `performLogout` hardcodes `window.location.href = "/"`.** On an SSO client `/`
618
+ re-triggers the IdP, so logout loops and `/login` is unreachable. An app-side override needs
619
+ **both** the logout action and `createAxiosInstance({ onLogout })` (the 401 path is separate) —
620
+ but that workaround was rejected in supply, so this one is blocking real behavior until fixed in
621
+ blox.
622
+
623
+ **Fixing these is blox-authoring work, tracked separately — see
624
+ [blox authoring defect backlog](../apps/toga-blox/features/blox-authoring-defects.md), which
625
+ carries the mechanism, consumer symptom and fix direction for each.** This is **not** app work. In a
626
+ consuming app, guard against them and move on.
603
627
 
604
628
  ---
605
629
 
@@ -640,6 +664,13 @@ app work.** In a consuming app, guard against them and move on.
640
664
 
641
665
  ## Change history
642
666
 
667
+ - 2026-08-27 — §13(a): corrected the caret rationale — a caret on a **prerelease is inert**, not
668
+ floating; the real floating risk is `toga2-commerce`'s `amplify.yml` overwriting the manifest pin
669
+ at build time. §22: added three blox auth gotchas found debugging toga25-supply SSO
670
+ (`handleClientAuthentication` name collision, `fetchPublicToken` persisting the public token to
671
+ `localStorage`, `performLogout`'s hardcoded `"/"`), and resolved the dangling
672
+ "a future `blox-authoring.md`" pointer to the new
673
+ [blox authoring defect backlog](../apps/toga-blox/features/blox-authoring-defects.md). (apeterson)
643
674
  - 2026-08-26 — Recorded commerce's missing `resolve.dedupe` (+ the blox-symlink second-React
644
675
  hazard), its caret blox pin and cross-branch pin drift, the barrel-import missing-peer trigger,
645
676
  and the removal of blox's phantom `react-router-dom` peer. The blox half is now merged and
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 69 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
- - **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
+ - **dbchanges2** (Database Changes) _(framework core)_ — 12 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
@@ -30,8 +30,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
30
30
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
31
31
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
32
32
  - **toga2-commerce** (TOGa Commerce) — 19 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
33
- - **toga25-supply** (TOGa 2.5 Supply) — 12 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
- - **toga-blox** (TOGa Blox) — 12 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
33
+ - **toga25-supply** (TOGa 2.5 Supply) — 13 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga-blox** (TOGa Blox) — 13 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
35
35
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
36
36
 
37
37
  ## standalone framework
@@ -21,6 +21,7 @@
21
21
  | [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
22
22
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
23
23
  | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
24
+ | [Granting a Compass user the same toga25-supply navigation as another user (role vs. SurfaceOverride)](workflows/granting-navigation-access.md) | 2.0 | "Give user X the same menu items user Y has" is the **navigation analog** of the recurring [bundle/persona grant](./granting-persona-bundle-access.md) request. | dbchanges2/Client_Compass/2026-08-27a - GrantAgilantAdministratorsRoleToInternalUser.sql |
24
25
  | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
25
26
  | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
26
27
  | [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: compass-usa
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-08-26
21
+ updated: 2026-08-27
22
22
  owners: [jcardinal, bala, tcox, apeterson, dfranks]
23
23
  files: []
24
24
  related:
@@ -28,6 +28,7 @@ related:
28
28
  - workflows/persona-refactor-migration.md
29
29
  - workflows/persona-population-env-comparison.md
30
30
  - workflows/granting-persona-bundle-access.md
31
+ - workflows/granting-navigation-access.md
31
32
  - features/mits-sales-order-transmission-alerting.md
32
33
  - features/asn-to-item-fulfillment.md
33
34
  - features/order-fulfillment-status-per-line.md
@@ -0,0 +1,158 @@
1
+ ---
2
+ title: Granting a Compass user the same toga25-supply navigation as another user (role vs. SurfaceOverride)
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-27
10
+ owners: [apeterson]
11
+ files:
12
+ - dbchanges2/Client_Compass/2026-08-27a - GrantAgilantAdministratorsRoleToInternalUser.sql
13
+ related:
14
+ - ./granting-persona-bundle-access.md
15
+ - ../profile.md
16
+ - ../../../2.0/apps/_underscore/features/surface-resolver.md
17
+ - ../../../2.0/apps/dbchanges2/features/surface-layer-schema.md
18
+ - ../../../2.0/apps/toga25-supply/features/surface-frontend.md
19
+ - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
20
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ "Give user X the same menu items user Y has" is the **navigation analog** of the recurring
26
+ [bundle/persona grant](./granting-persona-bundle-access.md) request. On Compass it is *not* an ACL
27
+ question at all — the `navigation` TAB_STRIP surface is gated **purely** by role-scoped
28
+ `Client_Compass.SurfaceOverrides` rows. Nothing about `AclActionPermissions` participates.
29
+
30
+ The trap is scope. The two users' role sets will differ by several roles, but usually only **one** of
31
+ those roles carries navigation overrides. Granting the whole role diff hands over far more than
32
+ navigation.
33
+
34
+ All facts below were verified read-only against live `Client_Compass` on **2026-08-27**.
35
+
36
+ ## How Compass navigation is actually gated
37
+
38
+ Two facts, both verified, that together mean the surface-override table is the *only* lever:
39
+
40
+ 1. **Every one of the six nav elements is seeded Core-base `isVisible = 0`.** Nothing is visible by
41
+ default; every visible menu item exists because some role turned it on.
42
+ 2. **Every one of them has `aclActionId = NULL`.** The element-level ACL gate inside
43
+ `resolve()` (`_Model_Client_AclActionPermission` → `aclActionId`, see
44
+ [Surface Resolver](../../../2.0/apps/_underscore/features/surface-resolver.md)) is therefore a
45
+ **no-op for navigation**. Do not go looking for a missing `AclActionPermissions` row; there was
46
+ never one to find.
47
+
48
+ So visibility = exactly one thing: a `Client_Compass.SurfaceOverrides` row with
49
+ `attribute = 'IS_VISIBLE'`, `value = 1`, scoped to a `roleId` the user holds.
50
+
51
+ ### `Core.SurfaceElements` ids for surface slug `navigation`
52
+
53
+ | Element id | `config.path` | Nav item | `sortOrder` |
54
+ |---|---|---|---|
55
+ | 109 | `sales-orders` | Sales Orders | 10 |
56
+ | 110 | `inventory` | Inventory | 20 |
57
+ | 111 | `items` | Items | 30 |
58
+ | 112 | `vendor-items` | Vendor Items | 40 |
59
+ | 113 | `bundles` | Bundles | 50 |
60
+ | 114 | `service-requests` | Service Requests | 60 |
61
+
62
+ > **⚠ These ids sit OUTSIDE the team-reserved blocks.** The reserved-id table in
63
+ > [Surface Resolver](../../../2.0/apps/_underscore/features/surface-resolver.md) and
64
+ > [surface-layer-schema](../../../2.0/apps/dbchanges2/features/surface-layer-schema.md) lists
65
+ > `Core.Surfaces` **41–43** / `Core.SurfaceElements` **125–146**. The `navigation` surface predates
66
+ > those blocks and lives at elements **109–114**. Read the reserved blocks as "the decision-surface
67
+ > reseed", not as an inventory of all surface ids.
68
+
69
+ ### The Compass override map (all `IS_VISIBLE`, `value = 1`, `personaId` NULL, `languageId` NULL)
70
+
71
+ | Element | Roles granting visibility |
72
+ |---|---|
73
+ | 109 Sales Orders | **4** (Admin), **8** (Manager), **9** (Agilant - Administrators) |
74
+ | 111 Items | **9** only |
75
+ | 112 Vendor Items | **9** only |
76
+ | 113 Bundles | **9** only |
77
+ | 110 Inventory | **none — no override row for any role** |
78
+ | 114 Service Requests | **none — no override row for any role** |
79
+
80
+ ## The diagnostic recipe ("same navigation as user Y")
81
+
82
+ 1. **Diff the two users' `Users_Roles` rows.** Here 289067 held roles 1, 4, 7, 12; the reference user
83
+ 127046 held 1, 4, 5, 7, 9, 12, 15, 16 — a **four-role** gap (5, 9, 15, 16).
84
+ 2. **Intersect that diff against the roles that actually appear in the nav override map.** Only role
85
+ **9** did. Roles 5, 15 and 16 carry **no** nav overrides and were irrelevant to the request. A
86
+ four-role gap collapsed to a one-role decision. **Never grant the whole role diff.**
87
+ 3. **Identify whose bundle you are looking at by counting visible elements.** If a developer pastes a
88
+ surface bundle without saying which user it came from, match the visible set against the override
89
+ map. A bundle showing **4** visible items (sales-orders / items / vendor-items / bundles) is role
90
+ 9's exact signature, so it belonged to 127046 — not to 289067, who holds only role 4 of the
91
+ nav-granting roles and therefore sees **Sales Orders alone**. Settling attribution first avoids
92
+ diagnosing the wrong user.
93
+ 4. **Pick the lever** (below), write the migration, hand it to the developer to run.
94
+
95
+ ### Two levers, and the tradeoff
96
+
97
+ | Lever | Blast radius | Use when |
98
+ |---|---|---|
99
+ | **(a) Grant the missing role** — one additive `Users_Roles` row | affects only this user, but confers **all** of that role's record-CRUD, field and action grants, not just nav | the user genuinely *is* that persona |
100
+ | **(b) Add role-scoped `SurfaceOverrides` rows** for a role the user already holds | surgical on nav, but widens nav for **every holder** of that role | the role is narrow and the nav change is intended tenant-wide |
101
+
102
+ Lever (b) against a broad role such as 4 (Admin) is a **tenant-wide behavior change** dressed up as a
103
+ per-user fix. For user 289067 the developer confirmed an internal Agilant user, so **(a)** was
104
+ correct: grant `Agilant - Administrators` additively.
105
+
106
+ ## The migration
107
+
108
+ `dbchanges2/Client_Compass/2026-08-27a - GrantAgilantAdministratorsRoleToInternalUser.sql` — additive
109
+ `Users_Roles` insert for user 289067; roles 1, 4, 7, 12 are left intact.
110
+
111
+ Three house-style points it demonstrates:
112
+
113
+ - **Resolve the role by name subselect, not a hardcoded id.** `Roles.id` differs per client DB, so the
114
+ file matches `WHERE r.name = 'Agilant - Administrators'`. The subselect stays entirely inside
115
+ `Client_Compass`, so the file names only tables in its own database and is cluster-safe.
116
+ - **Wrap the idempotency guard in a derived table.** The guard reads the INSERT's own target table,
117
+ which trips MySQL error 1093 (and, unwrapped, can see its own new row mid-statement):
118
+
119
+ ```sql
120
+ AND r.id NOT IN (
121
+ SELECT roleId FROM (
122
+ SELECT DISTINCT roleId FROM Users_Roles WHERE userId = 289067
123
+ ) AS existing
124
+ )
125
+ ```
126
+
127
+ Same dodge documented in
128
+ [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
129
+ - **Pre-generate the v4 UUID as a literal** — never `UUID()`.
130
+
131
+ ## Gotchas
132
+
133
+ - **⚠ Inventory (110) and Service Requests (114) are unreachable for the ENTIRE Compass tenant.** No
134
+ role — not even Agilant - Administrators — has an `IS_VISIBLE` override for them, and Core base is
135
+ `0`. Copying "the same nav as the admin user" will *not* surface them. This was found, not fixed, on
136
+ 2026-08-27 and needs its own ticket.
137
+ - **Role permissions resolve as a union**, so an additive `Users_Roles` row never takes access away —
138
+ see [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md). The flip
139
+ side is that it also grants everything else the role carries.
140
+ - **Verifying the change requires a LOGOUT, not a reload.** Surface meta is cached
141
+ `staleTime`/`gcTime` `Infinity` **and persisted to `localStorage`** under
142
+ `supply-chain-query-cache`, so a hard reload restores the stale bundle and proves nothing. There is
143
+ no server-side surface cache. Full detail in
144
+ [Surface Frontend](../../../2.0/apps/toga25-supply/features/surface-frontend.md).
145
+ - **`aclActionId` is NULL on all six nav elements** — resist the instinct to reach for
146
+ `AclActionPermissions` when a menu item is missing. It is always a `SurfaceOverrides` row.
147
+
148
+ ## Change history
149
+ - 2026-08-27 — Initial: documented that Compass `navigation` is gated **solely** by role-scoped
150
+ `SurfaceOverrides IS_VISIBLE` rows (all six elements are Core-base `isVisible = 0` with
151
+ `aclActionId = NULL`, so the element ACL gate is a no-op); recorded the element-id → role map
152
+ (109/111/112/113; role 9 is the only role granting Items/Vendor Items/Bundles) and that
153
+ `SurfaceElements` **109–114** sit **outside** the reserved 125–146 block. Added the diff-then-
154
+ intersect diagnostic recipe (a four-role gap for user 289067 collapsed to role 9 alone), the
155
+ visible-element-count trick for attributing a pasted bundle, and the role-grant vs. SurfaceOverride
156
+ tradeoff. Flagged that **Inventory and Service Requests are visible to no Compass role at all**.
157
+ Migration `2026-08-27a - GrantAgilantAdministratorsRoleToInternalUser.sql` written on branch
158
+ `TRUE-81325` — not committed, not executed. (apeterson)
@@ -11,6 +11,7 @@ owners: [bala]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
13
  related:
14
+ - ./granting-navigation-access.md
14
15
  - ../features/persona-model-and-levy-gating.md
15
16
  - ./persona-refactor-migration.md
16
17
  - ./persona-population-env-comparison.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.665",
3
+ "version": "1.0.666",
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",