toga-ai 1.0.902 → 1.0.904
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +3 -0
- package/knowledge/2.0/apps/toga2-commerce/features/force-logout-on-deployment.md +46 -0
- package/knowledge/2.0/apps/toga2-commerce/features/page-loading-transitions.md +39 -0
- package/knowledge/2.0/apps/toga2-commerce/features/sso-redirect-and-session-gating.md +1 -0
- package/knowledge/2.0/apps/toga2-commerce/workflows/local-development-hosts.md +37 -0
- package/knowledge/2.0/apps/toga25-supply/features/side-navigation-and-default-route.md +22 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-canada/INDEX.md +1 -0
- package/knowledge/clients/compass-canada/profile.md +2 -1
- package/knowledge/clients/compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md +88 -0
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/people-file-user-lifecycle.md +8 -5
- package/knowledge/clients/compass-usa/features/sso-devteam-user-shortcut.md +32 -0
- package/knowledge/clients/compass-usa/profile.md +1 -0
- package/knowledge/clients/compass-usa/workflows/granting-navigation-access.md +7 -10
- package/knowledge/clients/quad/INDEX.md +1 -0
- package/knowledge/clients/quad/profile.md +7 -1
- package/knowledge/clients/quad/workflows/granting-internal-admin-navigation.md +81 -0
- package/package.json +1 -1
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
|
|
14
14
|
| [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | Expedited shipping options are config-gated to show only when the cart holds a computer kit; open when touching cart shipping-method options or the gate config. |
|
|
15
15
|
| [Filter / Search-Results Page & the Two Search Entry Points](features/filter-search-results-page.md) | Two search entry points (Home in-place vs Header→/filter) render the same card via different code paths and FIELDS files; open for any search-results or kit-car |
|
|
16
|
+
| [Force Logout on Deployment (DeploymentGuard) — commerce](features/force-logout-on-deployment.md) | Signs every user out when a deploy bumps a Core parameter, after showing a notice; open for surprise logouts, a repeating "new version" toaster, or `useDeployme |
|
|
16
17
|
| ["Order For" — header pill, delegated token switch, and the cart hand-off](features/header-order-for-dropdown.md) | Header pill that switches the session to a delegated token so a buyer orders **as** someone else; open before touching the pill, `useOrderForZu`, the delegation |
|
|
17
18
|
| [Inactive-item purchase gating (standalone lines only — kits are exempt by design)](features/inactive-item-purchase-gating.md) | `Items.isActive = 0` blocks standalone purchase but means something different on kit components, so the gate keys on purchase CONTEXT not the flag; open when to |
|
|
18
19
|
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | How the hostname resolves the tenant and fans out to API base URL, theme (CSS variables), and fields; open when adding a tenant or debugging wrong-theme/wrong-A |
|
|
19
20
|
| [Order-submit sync sequencing (useSubmitOrder) — why these calls must not run in parallel](features/order-submit-sync-sequencing.md) | The item sync must finish before the header sync on order submit, or concurrent line deletes race a depth-3 response into an uncaught api2 500; open when touchi |
|
|
21
|
+
| [Skeleton → content transitions (AnimatePresence mode, layout jump, blank gap)](features/page-loading-transitions.md) | How commerce swaps a loading skeleton for real content with framer-motion; open for page jump/flicker or a blank gap while loading. |
|
|
20
22
|
| [Config-Driven Shipping Cost Waiver (Standard Ground free for computer kits)](features/shipping-cost-waiver-gating.md) | Config-driven cost waiver making Standard Ground free for a bare computer kit (Compass USA); open when touching shipping cost gates or the Compass free-freight |
|
|
21
23
|
| [SSO sign-in, session gating & token refresh identity (hasSession / useClientSsoUrl)](features/sso-redirect-and-session-gating.md) | How commerce decides whether a visitor is signed in, shows the SSO panel or the credential form, and keeps a token refresh bound to one identity. |
|
|
22
24
|
| [Storefront persona gating — the Home page's kit / category / item counts, and the persona switcher](features/storefront-persona-gating-and-home-counts.md) | What a storefront user sees on the Home page is decided **in the front end**, from `user._personaIds`, not by the API's ACL. |
|
|
@@ -24,4 +26,5 @@
|
|
|
24
26
|
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How toga2-commerce builds/deploys on AWS Amplify (one dynamic amplify.yml, branch-per-env, per-env blox channel); open to deploy, stand up an env, resolve a blo |
|
|
25
27
|
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) | Cypress e2e conventions + the Cart harness (cartV2.cy.ts) for toga2-commerce; read before adding any spec here, and note it doubles as the config-cart refactor |
|
|
26
28
|
| [Diagnosing ERR_HTTP2_PROTOCOL_ERROR (one client fails, everyone else is fine)](workflows/http2-protocol-error-diagnosis.md) | Diagnose ERR_HTTP2_PROTOCOL_ERROR when one client/network fails a togacommerce tenant but everyone else works (usually the client's TLS-inspection proxy, not ou |
|
|
29
|
+
| [Running toga2-commerce locally (tenant hosts, not localhost)](workflows/local-development-hosts.md) | Commerce cannot run on `localhost`; use a tenant hostname in `/etc/hosts`. |
|
|
27
30
|
| [Unit tests in toga2-commerce — vitest harness & the build-mode trap](workflows/unit-testing-vitest.md) | How to run and write unit tests in commerce (vitest + jsdom), and how to verify a change actually compiles without tripping the build-mode trap. |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Force Logout on Deployment (DeploymentGuard) — commerce
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-commerce/src/hooks/useDeploymentGuard.tsx
|
|
13
|
+
- toga2-commerce/src/lib/deploymentVersion.ts
|
|
14
|
+
- toga2-commerce/src/components/AuthLayout/AuthLayout.tsx
|
|
15
|
+
- toga2-commerce/src/contexts/AuthContext.tsx
|
|
16
|
+
- toga2-commerce/src/lib/authStorage.ts
|
|
17
|
+
related:
|
|
18
|
+
- ./sso-redirect-and-session-gating.md
|
|
19
|
+
- ./catalog-cache-freshness.md
|
|
20
|
+
- ../../toga25-supply/features/force-logout-on-deployment.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Signs every user out when a deploy bumps a Core parameter, after showing a notice; open for surprise logouts, a repeating "new version" toaster, or `useDeploymentGuard`.
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
Core parameter `META_LAST_REFRESH_DATETIME` is polled while signed in. When it changes mid-session the app raises a toaster ("A new version is available"), waits **2.5s**, then signs out and reloads — so tokens, `localStorage`, and the persisted React Query cache (`localStorage["commerce"]`) all reset to the new build.
|
|
28
|
+
|
|
29
|
+
Ported from toga25-supply (`force-logout-on-deployment`). Two commerce-only differences:
|
|
30
|
+
- **Mounted as `<DeploymentGuard />` inside `AuthLayout`**, not in `AuthContext`. `ToasterProvider` sits **below** `AuthProvider` in `App.tsx`, so the context that owns sign-out cannot raise a toaster; the guard component is inside both providers.
|
|
31
|
+
- **Baseline lives only in `localStorage["deployment-version"]`**, listed in `AUTH_STORAGE_KEYS` so logout clears it. No baseline → the check is skipped, which is what stops a sign-out loop.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
1. Query `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` **5 minutes**. The old version refetched only on navigation, so an idle tab never noticed a deploy.
|
|
36
|
+
2. First observation writes the baseline and never signs out.
|
|
37
|
+
3. A later differing value → toaster → 2.5s timer → `logout()` + reload.
|
|
38
|
+
4. `logout()` clears `AUTH_STORAGE_KEYS`, including the baseline, so the next login just re-baselines.
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
|
|
42
|
+
- **⚠ Do not depend an effect on `addToaster`.** It is rebuilt on every `ToasterProvider` render, and raising the toaster re-renders that provider — the effect re-ran and its cleanup cleared the `setTimeout` it had just set, so the notice showed and the sign-out never fired. Keep unstable callbacks in refs and depend only on the stable value.
|
|
43
|
+
- **⚠ The "already fired" flag must be module scope, not `useRef`.** The guard is mounted inside `AuthLayout`; a route change remounts it and a per-mount ref resets to `false`, so the notice reappeared on every navigation. Also do not clear the timer on unmount.
|
|
44
|
+
|
|
45
|
+
## Change history
|
|
46
|
+
- 2026-09-25 — Built for commerce: moved out of `AuthContext` into `<DeploymentGuard />` so a toaster can be shown; added 5-min poll + focus refetch; baseline key added to `AUTH_STORAGE_KEYS`. TRUE-81991. (apeterson)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Skeleton → content transitions (AnimatePresence mode, layout jump, blank gap)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-commerce/src/pages/Account/AccountPage.tsx
|
|
13
|
+
- toga2-commerce/src/pages/Home/HomePage.tsx
|
|
14
|
+
- toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ./storefront-persona-gating-and-home-counts.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
How commerce swaps a loading skeleton for real content with framer-motion; open for page jump/flicker or a blank gap while loading.
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
Page-level loading states are framer-motion `AnimatePresence` swaps between a shimmer skeleton and the loaded content, using `fadeInOutVariants` (exit duration **0.3s**).
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
`AnimatePresence` defaults to `mode="sync"`: the exiting child stays in layout while the entering child mounts, so both heights sum, then drop. Page-level swaps must use **`mode="wait"`**.
|
|
29
|
+
|
|
30
|
+
Measured on a cold `/account` load with `sync`: page height 1300 → **1962** (content mounts while the 520px shimmer is still in flow) → 1442 when the shimmer is finally removed. The 305ms between the two jumps matches the 0.3s exit exactly. With `mode="wait"`: 1300 → 1442, one step.
|
|
31
|
+
|
|
32
|
+
## Gotchas
|
|
33
|
+
|
|
34
|
+
- **⚠ `mode="sync"` (the default) double-counts height** on any skeleton→content swap. Two visible jumps.
|
|
35
|
+
- **⚠ Open — HomePage shows a blank white gap, not a jump.** `src/pages/Home/HomePage.tsx` (~line 488) renders `{!isLoading && (<AnimatePresence>{filteredItems}</AnimatePresence>)}`. `isLoading` comes from `homeDataState.status !== "ready"` in `useHomeViewModel` and flips false **before** `filteredItems` is populated, so the skeleton unmounts with nothing to replace it. Frame-by-frame: 4.10s skeleton cards → 4.30s blank card area → 4.40s real cards. Fix belongs in the view model's readiness logic — and must not treat "filteredItems is empty" as not-ready, or a legitimate no-results state renders a skeleton forever.
|
|
36
|
+
- **⚠ The `mode="wait"` fix is not shipped.** It sits in `git stash@{0}` in toga2-commerce as of 2026-09-25, unreviewed.
|
|
37
|
+
|
|
38
|
+
## Change history
|
|
39
|
+
- 2026-09-25 — Documented `AnimatePresence` sync-vs-wait layout jump (measured) and the still-open HomePage blank gap. (apeterson)
|
|
@@ -87,5 +87,6 @@ api2 **rotates** the refresh token. Concurrent 401s each spent a token the other
|
|
|
87
87
|
|
|
88
88
|
## Change history
|
|
89
89
|
- 2026-09-25 — Recorded that `GET /domains` is cached (`Core.Records` id 1, `ttlCache = 15`), the likely reason freshly inserted SSO rows still fall through to `/login` (rgirish)
|
|
90
|
+
- 2026-09-25 — Deep links now survive sign-in: `PrivateRoute` saves before redirecting, one consumer per path, `onSuccess` navigates removed; header/footer logos point at `/home`. Known gap: the saml gateway always returns to `/`. TRUE-81991. (apeterson)
|
|
90
91
|
- 2026-09-22 — Public token moved out of `localStorage`; `hasSession()` + identity epoch + `AUTH_STORAGE_KEYS` added; SSO sign-in screen replaces the auto IdP jump; open-redirect and SSO-URL-scheme holes closed; single-flight refresh. TRUE-81991 (`e546db3d`). (apeterson)
|
|
91
92
|
- 2026-09-21 — Blank page at `/` fixed: gate on stored `user.uuid`, `AuthRedirect` renders the spinner instead of `null`. Branch `TRUE-82172` (`1eebdd96`, `3dc53d9a`), **not merged or deployed**. (tcox)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Running toga2-commerce locally (tenant hosts, not localhost)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga2-commerce/vite.config.ts
|
|
13
|
+
- toga2-commerce/package.json
|
|
14
|
+
related:
|
|
15
|
+
- ../architecture.md
|
|
16
|
+
- ./amplify-build-and-deploy.md
|
|
17
|
+
- ../features/sso-redirect-and-session-gating.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Commerce cannot run on `localhost`; use a tenant hostname in `/etc/hosts`. Open when local dev bounces straight to `/login`.
|
|
21
|
+
|
|
22
|
+
## Steps
|
|
23
|
+
|
|
24
|
+
1. Add to `/etc/hosts`, pointing at `127.0.0.1`: `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce` (these are the exact hosts allowed in `vite.config.ts`). For `npm run sandbox-client` also add `client.sandbox.togacommerce`.
|
|
25
|
+
2. Start Vite and open the tenant host, never `http://localhost:<port>`.
|
|
26
|
+
|
|
27
|
+
## Why localhost fails
|
|
28
|
+
|
|
29
|
+
The tenant is resolved from `window.location.hostname`, and api2 checks the request `Origin` against `Core.Domains`. An unregistered origin returns **401** → the axios interceptor logs out → the app lands on `/login` with storage cleared. Copying tokens from another origin into `localStorage` does not help.
|
|
30
|
+
|
|
31
|
+
## Gotchas
|
|
32
|
+
|
|
33
|
+
- **⚠ `npm run sandbox-client` dies with `getaddrinfo ENOTFOUND`** until `client.sandbox.togacommerce` has its own hosts entry.
|
|
34
|
+
- **⚠ A missing hosts entry looks like an auth bug.** The symptom is an instant redirect to `/login`, not a network error.
|
|
35
|
+
|
|
36
|
+
## Change history
|
|
37
|
+
- 2026-09-25 — Documented after tracing "cannot sign in locally" to the Origin/`Core.Domains` check. (apeterson)
|
|
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-09-28
|
|
10
|
+
owners: [bala, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/routes.tsx
|
|
13
13
|
- toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts
|
|
@@ -15,10 +15,12 @@ files:
|
|
|
15
15
|
- _underscore/Model/Core/Surface.php
|
|
16
16
|
related:
|
|
17
17
|
- ./surface-frontend.md
|
|
18
|
+
- ./transfer-orders-page.md
|
|
18
19
|
- ../../_underscore/features/surface-resolver.md
|
|
19
20
|
- ../../_underscore/features/acl-permission-chain.md
|
|
20
21
|
- ../../../1.0/apps/tools/features/persona-gated-navigation.md
|
|
21
22
|
- ../../../../clients/compass-usa/workflows/granting-navigation-access.md
|
|
23
|
+
- ../../../../clients/quad/workflows/granting-internal-admin-navigation.md
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
2.5 nav/routes/landing are 100% backend-driven by the `navigation` surface; a user with zero visible nav elements gets a BLANK page reported as "can't log in" — open when a login "fails" but auth actually succeeded.
|
|
@@ -32,6 +34,21 @@ Durable lesson: a user whose roles grant **zero** visible nav elements gets a **
|
|
|
32
34
|
## How it works
|
|
33
35
|
|
|
34
36
|
1. `useAppLayoutViewModel` fetches `GET /v2/surfaces/meta?slug=navigation` and builds `sideNavItems` from the bundle's elements: `elements.filter((el) => el.isVisible && elementPath(el))`, sorted by `sortOrder`. `isVisible` comes from the server-side `SurfaceOverrides` cascade — see [Surface Resolver](../../_underscore/features/surface-resolver.md).
|
|
37
|
+
|
|
38
|
+
**Every nav element ships Core-base `isVisible = 0`**, so with no override the entry is hidden for
|
|
39
|
+
everyone. The canonical `Core.SurfaceElements` map for `Core.Surfaces.id = 39` (slug `navigation`) —
|
|
40
|
+
the ids you need to write an override row:
|
|
41
|
+
|
|
42
|
+
| Element id | `config.path` | Nav item | `sortOrder` |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| 109 | `sales-orders` | Sales Orders | 10 |
|
|
45
|
+
| 110 | `inventory` | Inventory | 20 |
|
|
46
|
+
| 111 | `items` | Items | 30 |
|
|
47
|
+
| 112 | `vendor-items` | Vendor Items | 40 |
|
|
48
|
+
| 113 | `bundles` | Bundles | 50 |
|
|
49
|
+
| 114 | `service-requests` | Service Requests | 60 |
|
|
50
|
+
| 190 | `transfer-orders` | Transfer Orders | — |
|
|
51
|
+
|
|
35
52
|
2. It exposes `sideNavPaths` (same list, leading slash stripped) plus `isNavReady = !isLoading && surfaceMeta !== null`.
|
|
36
53
|
3. `routes.tsx` → `AllowedRoutes` maps `sideNavPaths` through a **`ROUTE_REGISTRY`** keyed by path segment (`sales-orders`, `items`, `inventory`, `vendor-items`, `bundles`, `service-requests`), drops unknown keys, and takes the **first survivor** as the landing route:
|
|
37
54
|
|
|
@@ -79,3 +96,6 @@ That trailing silence is the tell: the app never picked a page, so it never aske
|
|
|
79
96
|
- **`isNavReady` is not an ACL signal, it is a "bundle arrived" signal.** `routes.tsx` aliases it as `isAclReady`, which reads like a permission check but is only `!isLoading && surfaceMeta !== null`. An empty-but-successful bundle is "ready".
|
|
80
97
|
- **Do not chase `AclActionPermissions` for a missing 2.5 menu item.** The `navigation-*` `Core.AclActions` rows are inert here — no `SurfaceElement` on `appId = 1` declares an `aclActionId`. The only lever is a role-scoped `SurfaceOverrides` `IS_VISIBLE = '1'` row. Full reasoning in [acl-permission-chain](../../_underscore/features/acl-permission-chain.md).
|
|
81
98
|
- **Verifying a nav fix requires a LOGOUT, not a reload.** The surface bundle is cached with `staleTime`/`gcTime` `Infinity` and persisted to `localStorage` under `supply-chain-query-cache`. See [Surface Frontend](./surface-frontend.md).
|
|
99
|
+
|
|
100
|
+
## Change history
|
|
101
|
+
- 2026-09-28 — Added the canonical `Core.SurfaceElements` id → path map for surface 39 (incl. 190 `transfer-orders`); it was previously only in the Compass workflow doc (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -22,7 +22,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
22
22
|
- **_underscore** (_Underscore) _(framework core)_ — 90 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
23
23
|
- **worker2** (Worker) — 80 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
24
24
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
25
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
25
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 21 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
26
26
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
27
27
|
- **saml** (SAML SSO Gateway) — 9 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
28
28
|
- **apiproxy** (API Proxy) — 1 doc(s) → [2.0/apps/apiproxy/INDEX.md](2.0/apps/apiproxy/INDEX.md)
|
|
@@ -32,7 +32,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
32
32
|
- **talos-backend** (TOGa Talos Backend) — 1 doc(s) → [2.0/apps/talos-backend/INDEX.md](2.0/apps/talos-backend/INDEX.md)
|
|
33
33
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
34
34
|
- **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
35
|
-
- **toga2-commerce** (TOGa Commerce) —
|
|
35
|
+
- **toga2-commerce** (TOGa Commerce) — 29 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
36
36
|
- **toga25-supply** (TOGa 2.5 Supply) — 22 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
37
37
|
- **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
|
|
38
38
|
- **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
|
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
| [Compass Canada — Item-Fulfillment TableViews (dead-Core-id 500, rebuilt onto the USA shape)](features/item-fulfillment-tableviews.md) | 2.0 | Why Compass Canada's item-fulfillment TableViews 500'd on dead Core ids and how they were rebuilt onto the USA shape; open when a Canada TableView 500s on /meta |
|
|
9
9
|
| [Compass Canada — Fulfilled vs Partially Fulfilled (per-line, ASN bridge only)](features/order-fulfillment-status-per-line.md) | 2.0 | Compass Canada's pure per-line fulfilled/partiallyFulfilled rule (ASN bridge only) and why it diverges from USA; open before touching Canada `_fulfillmentStatus |
|
|
10
10
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. |
|
|
11
|
+
| [Copying a Compass USA user into the Canada tenant (cross-tenant hand-add)](workflows/copying-a-us-user-into-the-canada-tenant.md) | 2.0 | Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops |
|
|
11
12
|
| [Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)](workflows/grand-and-toy-asn-backfill.md) | 2.0 | How to recover Compass Canada shipments whose ASN never landed (cXML replay, CSV-to-JSON, or SQL line backfill); open when replaying a G&T outage or repairing a |
|
|
@@ -18,7 +18,7 @@ project: _Underscore
|
|
|
18
18
|
client: compass-canada
|
|
19
19
|
type: profile
|
|
20
20
|
status: active
|
|
21
|
-
updated: 2026-09-
|
|
21
|
+
updated: 2026-09-28
|
|
22
22
|
owners: [jcardinal, bala, tcox, apeterson, ajean, mhammontree, rgirish]
|
|
23
23
|
files: []
|
|
24
24
|
related:
|
|
@@ -26,6 +26,7 @@ related:
|
|
|
26
26
|
- ../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md
|
|
27
27
|
- features/grand-and-toy-asn-import.md
|
|
28
28
|
- workflows/grand-and-toy-asn-backfill.md
|
|
29
|
+
- workflows/copying-a-us-user-into-the-canada-tenant.md
|
|
29
30
|
- ../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
|
|
30
31
|
- ../compass-usa/features/approval-decision-flow.md
|
|
31
32
|
- ../compass-usa/features/stranded-approval-reassignment.md
|
package/knowledge/clients/compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Copying a Compass USA user into the Canada tenant (cross-tenant hand-add)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: compass-canada
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-28
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_CompassCanada/2026-09-28a - AddJamesFullerSuperUser.sql
|
|
13
|
+
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
14
|
+
related:
|
|
15
|
+
- ../profile.md
|
|
16
|
+
- ../../compass-usa/features/people-file-user-lifecycle.md
|
|
17
|
+
- ../../compass-usa/workflows/granting-persona-bundle-access.md
|
|
18
|
+
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
20
|
+
---
|
|
21
|
+
Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops the next PEOPLE run switching them off.
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
US and Canada are **separate tenants** (`Client_Compass` / `Client_CompassCanada`). A US person has no Canada row at all, so "give James access to the Canada portal" is a **hand-add**, not a permission change. Two artifacts are always needed together:
|
|
25
|
+
|
|
26
|
+
1. A guarded, re-runnable `dbchanges2/Client_CompassCanada/<date><letter> - ….sql` that builds the row chain.
|
|
27
|
+
2. A one-line edit to `ALWAYS_ACTIVE_EMAILS_CA` in `worker2/Worker/Client/Compass/PeopleFile.php`.
|
|
28
|
+
|
|
29
|
+
Skip step 2 and the person is switched off on the next nightly PEOPLE run. The importer routes rows by `erp_system_id` (**1001** = US, **2010** = Canada), so a `@compass-usa.com` person only ever appears on the US side and the Canada inactivation sweep treats the new row as a leaver.
|
|
30
|
+
|
|
31
|
+
Template: `2026-09-28a - AddJamesFullerSuperUser.sql` (James Fuller, `Client_Compass` user id 1851). It mirrors how Gina Prince (CA user 22897) and Kathy Sweda (CA user 22898) were added.
|
|
32
|
+
|
|
33
|
+
## Steps
|
|
34
|
+
|
|
35
|
+
### 1. Read the US row, then check Canada first
|
|
36
|
+
Pull `Users` + roles + personas for the person from prod `Client_Compass`. Then **query `Client_CompassCanada.Users` by email before writing anything** — there is no unique key on `Users.email` in either tenant, so the database will happily accept a second account. (Index/collation facts: [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).)
|
|
37
|
+
|
|
38
|
+
### 2. Insert the chain in this order
|
|
39
|
+
`Client_CompassCanada.Users.contactId` is **NOT NULL**, and `Contacts` has **no email column** — the address lives in `ContactEmailAddresses` (`id, uuid, contactId, name, emailAddress`) and is pointed at by `Contacts.primaryContactEmailAddressId`. So:
|
|
40
|
+
|
|
41
|
+
1. `Contacts`
|
|
42
|
+
2. `ContactEmailAddresses`
|
|
43
|
+
3. `UPDATE Contacts SET primaryContactEmailAddressId = …` (guard on `IS NULL`)
|
|
44
|
+
4. `Users` (`contactId`, `email`, `c_hrEmpUsername`, `isActive = 1`)
|
|
45
|
+
5. `Users_Roles` — one row per role
|
|
46
|
+
6. `Users_Personas` — one row per persona
|
|
47
|
+
7. optional `UserGlobalSettings` language row (`settingId = 2`, value `'en'` or `'fr-CA'`)
|
|
48
|
+
|
|
49
|
+
Guard every statement (`NOT EXISTS`, `IS NULL`) so the file is re-runnable — pattern: [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md). **Guard the `Users` insert on `email`, not on the new uuid**, otherwise a re-run with a regenerated uuid creates a second account.
|
|
50
|
+
|
|
51
|
+
### 3. Map roles and personas by **uuid**, never by id
|
|
52
|
+
Role ids differ between the two tenants. Look up `(SELECT r.id FROM Roles r WHERE r.uuid = …)` inline. Prod-verified 2026-09-28:
|
|
53
|
+
|
|
54
|
+
| Role | uuid | US id | CA id |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| Base | `61d79bfb-4182-2c7a-6217-2510d6766bf1` | 1 | 1 |
|
|
57
|
+
| Admin | `0f7b101b-8050-4036-9129-84bc822f60f3` | 4 | 5 |
|
|
58
|
+
| SuperUser | `d11ce7ac-ed88-4f85-aba9-6c2b1d1e09fd` | 5 | 6 |
|
|
59
|
+
| Contractor | `1003f7a1-e798-11ee-b854-0aae2980db55` | 6 | 7 |
|
|
60
|
+
| Compass Base | `22c90415-0179-0445-c782-a02e066e00f9` | 7 | 8 |
|
|
61
|
+
| Manager | `459bbf3b-1eca-c049-040c-c492a6beee0f` | 8 | 9 |
|
|
62
|
+
|
|
63
|
+
`PeopleFile.php` already keeps `ROLE_UUID_BASE` / `ROLE_UUID_COMPASS_BASE` / `ROLE_UUID_MANAGER` constants for the same reason — reuse those values.
|
|
64
|
+
|
|
65
|
+
**Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest catalogue — 213 items, all 35 bundles; this is Gina Prince's) and **Managers/VIPs** id 33, uuid `7961068f-8b31-1daf-5f52-0e45a09beb98` (168 items, 21 bundles). Persona **All** is id 1. Persona grants resolve through three sources, not just `Users_Personas` — see [Granting persona/bundle access](../../compass-usa/workflows/granting-persona-bundle-access.md).
|
|
66
|
+
|
|
67
|
+
### 4. Add the address to `ALWAYS_ACTIVE_EMAILS_CA`
|
|
68
|
+
In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACTIVE_EMAILS_CA` — **not** `ALWAYS_ACTIVE_EMAILS_BOTH`. The BOTH list's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. `isEmailAlwaysActive()` lowercases its input, so a mixed-case constant entry never matches. Both layers of the guard are described in [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).
|
|
69
|
+
|
|
70
|
+
### 5. Review and hand over
|
|
71
|
+
Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change, then give the developer numbered run steps (SQL file + environment order, then the worker2 deploy).
|
|
72
|
+
|
|
73
|
+
## Gotchas
|
|
74
|
+
- **`Client_CompassCanada.Users` has no `username`, `isSuperUser`, `personaId` or `roleId` column.** Access is entirely `Users_Roles` + `Users_Personas`. The login/personnel username is `c_hrEmpUsername`, stored lowercase.
|
|
75
|
+
- **Leave `Users.password` NULL.** Login is SSO on the email address: only **45 of 28,788** `Client_CompassCanada` users hold a password, and the US rows for these people are NULL too.
|
|
76
|
+
- **`Users.email` uses a case-insensitive collation**, so the stored case does not affect matching — `Client_Compass` often stores these addresses uppercase while Canada stores them lowercase.
|
|
77
|
+
- **The Tools Compass admin cannot do this.** Activating a user outside the cron is temporary; the hardcoded constant is the only durable hold. Adding the person there instead of in the constant is the common mistake.
|
|
78
|
+
- **Keep the file inside `Client_CompassCanada`.** Never join `Client_Compass` and `Client_CompassCanada` in one statement — separate prod clusters. Read the US row, then write Canada-only SQL with the values baked in.
|
|
79
|
+
|
|
80
|
+
## Related
|
|
81
|
+
- [Compass Canada profile](../profile.md)
|
|
82
|
+
- [Compass PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md)
|
|
83
|
+
- [Granting a Compass user a bundle/persona and SuperUser](../../compass-usa/workflows/granting-persona-bundle-access.md)
|
|
84
|
+
- [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md)
|
|
85
|
+
- [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)
|
|
86
|
+
|
|
87
|
+
## Change history
|
|
88
|
+
- 2026-09-28 — Created from the James Fuller hand-add; records the uuid role/persona map and the `ALWAYS_ACTIVE_EMAILS_CA`-not-BOTH rule (bala)
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. |
|
|
28
28
|
| [Compass Sales-Order Line-Number Renumbering (and why it hangs off the SO hooks)](features/sales-order-line-renumbering.md) | 2.0 | How Compass keeps SO lines numbered 1..N with no gaps, why the renumber rides the parent SO hooks, and the two-phase SQL that avoids a duplicate-key fatal; open |
|
|
29
29
|
| [Compass SAML SSO (Entra) — identity mapping, the @togatech.com bypass, and per-environment enablement](features/saml-sso.md) | 2.0 | How Compass users are matched from an Entra SAML assertion, the hardcoded `@togatech.com` test bypass that is live in production, and what it takes to turn SSO |
|
|
30
|
+
| [Compass SSO signs every @togatech.com user in as one hardcoded user (GINA PRINCE, 4273)](features/sso-devteam-user-shortcut.md) | 2.0 | Why an internal dev signing into a Compass app via SSO lands as someone else; open for "I'm signed in as the wrong user" on Compass. |
|
|
30
31
|
| [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | The engine that repoints approvals stranded on dead duplicate Compass Users rows onto the live account; open when approvals are stuck on a re-hired employee's o |
|
|
31
32
|
| [Compass: where a `TrackingNumbers` row comes from (invoice sync, NOT the ASN) and how it reaches a customer](features/tracking-number-origin-and-asn-link.md) | 1.0 | Why a Compass TrackingNumbers row is born from the invoice sync (not the ASN) and why `dtCreated` is the wrong clock for any shipping notification/SLA; open bef |
|
|
32
33
|
| [Compass USA](profile.md) | 2.0 | Compass USA client profile: platforms, vendors/integrations, and the key features/traps across its 1.0+2.0 supply-chain commerce stack; open first when starting |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-28
|
|
10
10
|
owners: [bala, ajean, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
@@ -21,6 +21,7 @@ related:
|
|
|
21
21
|
- ../profile.md
|
|
22
22
|
- ../../../2.0/apps/_underscore/features/record-change-audit-log.md
|
|
23
23
|
- ../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md
|
|
24
|
+
- ../../compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md
|
|
24
25
|
---
|
|
25
26
|
The nightly Compass PEOPLE-file cron's Users-row lifecycle (insert/reactivate/deactivate), why it creates duplicate accounts, and its raw-SQL/index traps; open before touching PeopleFile.php or debugging stranded approvals.
|
|
26
27
|
|
|
@@ -69,13 +70,15 @@ Within one job the **inserts (and their id refresh) run before the inactivation
|
|
|
69
70
|
### Always-active protection is per-tenant, two layers
|
|
70
71
|
A small hardcoded set of people must stay active regardless of what HR sends. Since **2026-09-16** that set is split into two class constants on `_Worker_Client_Compass_PeopleFile`:
|
|
71
72
|
- **`ALWAYS_ACTIVE_EMAILS_BOTH`** — the original four addresses (two `@compass-usa.com`, two `@compass-canada.com`). Held active in **both** `Client_Compass` and `Client_CompassCanada`.
|
|
72
|
-
- **`ALWAYS_ACTIVE_EMAILS_CA`** —
|
|
73
|
+
- **`ALWAYS_ACTIVE_EMAILS_CA`** — three **Compass USA** staff held active in **`Client_CompassCanada` only**: `kaylin.shutte@` / `kate-lynn.racer@compassdigital.io` (usernames `shuttk01` / `racerk01`) and, since 2026-09-28, `james.fuller@compass-usa.com` (`fullej01`, US user id 1851). They are allowed to go inactive in the US tenant when the PEOPLE file drops them; that is the requirement, not a bug.
|
|
74
|
+
|
|
75
|
+
**Which list to use is the decision that matters.** A US-only person hand-added to Canada belongs in **CA**, never in BOTH — BOTH's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. Entries must be **lowercase**: `isEmailAlwaysActive()` lowercases its input and compares strictly. Full hand-add procedure: [Copying a US user into the Canada tenant](../../compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md).
|
|
73
76
|
|
|
74
77
|
`isEmailAlwaysActive(string $email, string $tenantKey): bool` is the single decision point — a lowercased match that returns true for the BOTH list in either tenant, and for the CA list only when `$tenantKey === 'CA'`. Two consumers:
|
|
75
78
|
1. **The inactivation loop** in `executeForTenant()` skips any user whose `Users.email` is protected for that tenant, so the batch `UPDATE ... SET isActive = 0` never lists them.
|
|
76
79
|
2. **`ensureCrossRegionUsersActive()`**, which runs after both tenants execute and force-sets `isActive = 1` — in both databases for the BOTH list, and in `Client_CompassCanada` only for the CA list.
|
|
77
80
|
|
|
78
|
-
**
|
|
81
|
+
**For the `@compassdigital.io` pair, only layer 2 is live.** In `Client_CompassCanada` both of those users have `c_hrEmpUsername = NULL`, and `loadLookups()` keys its map on username — so they never enter the map and can never reach the inactivation batch on the Canada side. (Read-only prod check, 2026-09-16.) **James Fuller is the opposite case**: his Canada row carries `c_hrEmpUsername = 'fullej01'`, so he *does* enter the map and **layer 1 is what actually saves him** — the inactivation loop guard is not theoretical.
|
|
79
82
|
|
|
80
83
|
**The email match works across a case difference.** `Client_Compass` stores these two addresses UPPERCASE while `Client_CompassCanada` stores them lowercase. `Users.email` uses `utf8mb4_0900_ai_ci`, a case-insensitive collation, so `WHERE email = '...'` still hits — each address resolves to exactly **one** row per database.
|
|
81
84
|
|
|
@@ -105,8 +108,8 @@ The insert-time hook designed 2026-08-12 was built and grew into a reusable engi
|
|
|
105
108
|
- **⚠ The `Users` ↔ `Contacts` email/phone mirror is reached through the PRIMARY pointers, not through `name = 'WORK'`.** `PeopleFile.php:300-310` joins on `Contacts.primaryContactEmailAddressId` / `Contacts.primaryContactPhoneNumberId`. `name = 'WORK'` is only set at **insert**, and there is **no unique key on `(contactId, name)`** — so a contact can hold several `WORK` rows and "the WORK row" is **not deterministic**. Follow the primary pointers; never match on the name.
|
|
106
109
|
- **⚠ The importer re-syncs the `Contacts` child rows UNCONDITIONALLY for every existing user.** `PeopleFile.php:630-632` runs **outside** the `hasDiff` gate, so a `Contacts` email/phone edit made by hand or by a tool is reverted on the next run **even when the `Users` row itself does not diff**. Any out-of-band contact edit is temporary in exactly the same way an out-of-band activation is.
|
|
107
110
|
- **⚠ `executeForTenant()`'s inactivation sweep is the ONLY place in the file that writes `isActive = 0`.** Every other `isActive` path in the ~2,200-line class only forces it back to **1**. Two consequences: (a) **an activation made outside this cron is temporary** — the next run deactivates the person again unless they are in that day's PEOPLE file — while (b) **a deactivation is durable**, because nothing here ever reactivates anyone on its own. This makes the [Tools Compass admin pages](../../../1.0/apps/tools/features/compass-user-persona-admin.md) a one-way-durable tool.
|
|
108
|
-
- **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no `isActive` filter**, so it reactivates **every** row carrying a protected address, dead duplicates included. Verified prod 2026-09-02 for the original four, and 2026-09-16 for the two CA-only addresses: each resolves to exactly **one** row per database, so nothing is being resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list is also the **only** durable way to hold a cross-region user active, and changing it needs a code change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list instead of matching on email** — matching on email means an email edit silently moves a user in or out of the protected set, which is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks the protected addresses from being edited at all.
|
|
109
|
-
- **⚠ The Tools admin's edit block covers only the original four addresses, not the
|
|
111
|
+
- **`ensureCrossRegionUsersActive()` runs `UPDATE Users SET isActive = 1 WHERE email = X` with no `isActive` filter**, so it reactivates **every** row carrying a protected address, dead duplicates included. Verified prod 2026-09-02 for the original four, and 2026-09-16 for the first two CA-only addresses: each resolves to exactly **one** row per database, so nothing is being resurrected today — but the query would happily do it if a duplicate appeared. That hardcoded list is also the **only** durable way to hold a cross-region user active, and changing it needs a code change and a deploy per request. **Recommended (not done): scope it to a `Users.id` list instead of matching on email** — matching on email means an email edit silently moves a user in or out of the protected set, which is why the [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) blocks the protected addresses from being edited at all.
|
|
112
|
+
- **⚠ The Tools admin's edit block covers only the original four addresses, not the CA-only ones.** The protected set grew to six in worker2 on 2026-09-16 and to **seven** on 2026-09-28; the `tools` block list was not changed and was not re-checked. Until it is, someone can edit the email of a CA-protected user in [Tools Compass admin](../../../1.0/apps/tools/features/compass-user-persona-admin.md) and silently drop them out of the protected set — because the match is on email, not id.
|
|
110
113
|
- **⚠ Adding a new "always keep this person active" rule means touching exactly two places.** `executeForTenant()`'s inactivation loop is the only `isActive = 0` writer, and `ensureCrossRegionUsersActive()` is the post-run sweep. Guard both and the requirement is covered; guard only the loop and the post-run sweep still decides the final state; guard only the sweep and the user flickers inactive between the two steps.
|
|
111
114
|
|
|
112
115
|
- **⚠ A whole tenant run is ONE transaction, and US runs first — so a US exception means Canada never executes at all.** Any throw rolls back everything for that run: inserts, updates, reactivations, **deactivations**, personas, roles and approval reassignment. The 2026-09-22 FK failure blocked both tenants for a day this way. Rollback is clean (proven: a run that died on `Users_Personas` after the `Users` insert left 0 rows behind), and the exception aborts **before** the SFTP delete, so the input file survives and the next run reprocesses it.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compass SSO signs every @togatech.com user in as one hardcoded user (GINA PRINCE, 4273)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-usa
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-09-25
|
|
9
|
+
owners: [apeterson]
|
|
10
|
+
files:
|
|
11
|
+
- _underscore/Model/Compass/ClientAuthentication.php
|
|
12
|
+
related:
|
|
13
|
+
- ../profile.md
|
|
14
|
+
- ../../../2.0/apps/toga2-commerce/features/sso-redirect-and-session-gating.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Why an internal dev signing into a Compass app via SSO lands as someone else; open for "I'm signed in as the wrong user" on Compass.
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
`_Model_Compass_ClientAuthentication::getAuthenticatedSsoUser()` checks the SAML NameID. If it contains `@togatech.com` it returns `new _Model_Compass_User(4273)` (**GINA PRINCE**, `_isSuperAdmin = 1`) instead of matching on `c_hrEmpUsername`. The source comment says "return gina's user if testing on devteam".
|
|
22
|
+
|
|
23
|
+
So **every** TOGA employee signing into **any** Compass app through SSO is that one shared super-admin user. Deliberate test shortcut, not a bug — but it costs real time to trace, because the behaviour starts in the front end and only shows up down in the framework model.
|
|
24
|
+
|
|
25
|
+
## Gotchas
|
|
26
|
+
|
|
27
|
+
- **⚠ Shared identity.** Two devs signed in at once are the same user — carts, orders and audit rows attribute to 4273.
|
|
28
|
+
- **⚠ Elevated by accident.** That user is a super admin, so devteam testing never sees normal-permission behaviour.
|
|
29
|
+
- **⚠ Not reachable by credentials.** Bypassing it needs a non-SSO sign-in path; see the proposed `/login?local=1` flag in the commerce SSO doc.
|
|
30
|
+
|
|
31
|
+
## Change history
|
|
32
|
+
- 2026-09-25 — Recorded after tracing a wrong-user SSO sign-in from commerce through the saml gateway into `_underscore`. (apeterson)
|
|
@@ -39,6 +39,7 @@ related:
|
|
|
39
39
|
- features/oneuptime-ma-refresh-order-monitor.md
|
|
40
40
|
- workflows/catalog-fold-reversal.md
|
|
41
41
|
- features/asn-to-item-fulfillment.md
|
|
42
|
+
- features/sso-devteam-user-shortcut.md
|
|
42
43
|
- features/odp-strategic-systems-asn-csv-import.md
|
|
43
44
|
- features/multi-tier-tracking-and-serial-mirroring.md
|
|
44
45
|
- features/order-fulfillment-status-per-line.md
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-28
|
|
10
10
|
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_Compass/2026-08-12a - NavigationRoleGrants.sql
|
|
@@ -16,6 +16,7 @@ files:
|
|
|
16
16
|
related:
|
|
17
17
|
- ./granting-persona-bundle-access.md
|
|
18
18
|
- ../profile.md
|
|
19
|
+
- ../../quad/workflows/granting-internal-admin-navigation.md
|
|
19
20
|
- ../../../2.0/apps/_underscore/features/surface-resolver.md
|
|
20
21
|
- ../../../2.0/apps/dbchanges2/features/surface-layer-schema.md
|
|
21
22
|
- ../../../2.0/apps/toga25-supply/features/surface-frontend.md
|
|
@@ -34,15 +35,7 @@ How to give a Compass user another user's toga25-supply menu items via role-scop
|
|
|
34
35
|
|
|
35
36
|
So visibility = exactly one thing: a `Client_Compass.SurfaceOverrides` row with `attribute = 'IS_VISIBLE'`, `value = 1`, scoped to a `roleId` the user holds.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
| Element id | `config.path` | Nav item | `sortOrder` |
|
|
39
|
-
|---|---|---|---|
|
|
40
|
-
| 109 | `sales-orders` | Sales Orders | 10 |
|
|
41
|
-
| 110 | `inventory` | Inventory | 20 |
|
|
42
|
-
| 111 | `items` | Items | 30 |
|
|
43
|
-
| 112 | `vendor-items` | Vendor Items | 40 |
|
|
44
|
-
| 113 | `bundles` | Bundles | 50 |
|
|
45
|
-
| 114 | `service-requests` | Service Requests | 60 |
|
|
38
|
+
Element ids for surface 39 (`navigation`) live in [side navigation & default route](../../../2.0/apps/toga25-supply/features/side-navigation-and-default-route.md) — do not duplicate them here.
|
|
46
39
|
|
|
47
40
|
The Compass override map (all `IS_VISIBLE`, `value = 1`, `personaId` NULL, `languageId` NULL):
|
|
48
41
|
| Element | Roles granting visibility |
|
|
@@ -122,6 +115,7 @@ Role 16's Vendor Items gap is **left open on purpose** — widening nav for ever
|
|
|
122
115
|
- **Role permissions resolve as a union**, so an additive `Users_Roles` row never takes access away — see [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md). The flip side is that it also grants everything else the role carries.
|
|
123
116
|
- **⚠ These ids sit OUTSIDE the team-reserved blocks.** The reserved-id table in [Surface Resolver](../../../2.0/apps/_underscore/features/surface-resolver.md) and [surface-layer-schema](../../../2.0/apps/dbchanges2/features/surface-layer-schema.md) lists `Core.Surfaces` **41–43** / `Core.SurfaceElements` **125–146**. The `navigation` surface predates those blocks and lives at elements **109–114**. Read the reserved blocks as "the decision-surface reseed", not as an inventory of all surface ids.
|
|
124
117
|
- **Verifying the change requires a LOGOUT, not a reload.** Surface meta is cached `staleTime`/`gcTime` `Infinity` **and persisted to `localStorage`** under `supply-chain-query-cache`, so a hard reload restores the stale bundle and proves nothing. There is no server-side surface cache. Detail in [Surface Frontend](../../../2.0/apps/toga25-supply/features/surface-frontend.md).
|
|
118
|
+
- **The `Agilant - Administrators` role may carry ZERO `AclFieldPermissions`.** `Client_Compass` role 9 has none — it is a nav/record-scoping role only; field reads come from the other roles the same user holds. `Client_Compasscanada` role **10** is the same role under a different id. Same pattern applied to Quad 2026-09-28: [Quad internal-admin navigation](../../quad/workflows/granting-internal-admin-navigation.md).
|
|
125
119
|
- **`aclActionId` is NULL on all six nav elements** — resist the instinct to reach for `AclActionPermissions` when a menu item is missing. It is always a `SurfaceOverrides` row.
|
|
126
120
|
|
|
127
121
|
## Related
|
|
@@ -133,3 +127,6 @@ Role 16's Vendor Items gap is **left open on purpose** — widening nav for ever
|
|
|
133
127
|
- [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md)
|
|
134
128
|
- [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)
|
|
135
129
|
- [Side navigation & default route](../../../2.0/apps/toga25-supply/features/side-navigation-and-default-route.md)
|
|
130
|
+
|
|
131
|
+
## Change history
|
|
132
|
+
- 2026-09-28 — Element-id map moved to the shared side-nav doc; recorded that role 9 holds no `AclFieldPermissions` and that Compass Canada's equivalent is role 10 (apeterson)
|
|
@@ -7,3 +7,4 @@
|
|
|
7
7
|
| [Quad: PO + ASN email importer (import_po_and_asn.php)](features/po-asn-email-import.md) | 1.0 | Quad's inbox-driven PO/ASN CSV importer and its 2026-08 failure classes; open when a Quad PO/ASN email jams the mailbox, fails EV-12, or you touch `import_po_an |
|
|
8
8
|
| [Quad Graphics](profile.md) | 2.0 | Quad Graphics is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Quad`. |
|
|
9
9
|
| [Quad Graphics — reversing the 30-item catalog fold (storefront-assortment recovery)](workflows/catalog-fold-reversal.md) | 2.0 | How the 2026-08-31 catalog-blind item merge damaged Quad's storefront and how the reversal was built; open when assessing/repairing catalog-fold damage on a two |
|
|
10
|
+
| [Giving Quad's internal TOGA staff extra toga25-supply nav (the `Agilant - Administrators` role pattern)](workflows/granting-internal-admin-navigation.md) | 2.0 | How to surface extra 2.5 Supply pages to internal TOGA staff on a tenant without showing them to the client's own users — Quad's first internal-admin role; open |
|
|
@@ -16,12 +16,13 @@ project: _Underscore
|
|
|
16
16
|
client: quad
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-09-
|
|
19
|
+
updated: 2026-09-28
|
|
20
20
|
owners: ["jcardinal", "bala", "apeterson", "ajean"]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
23
23
|
- ../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md
|
|
24
24
|
- ./workflows/catalog-fold-reversal.md
|
|
25
|
+
- ./workflows/granting-internal-admin-navigation.md
|
|
25
26
|
- ./features/po-asn-email-import.md
|
|
26
27
|
- ./features/enter-po-details-vendor-selection-ev12.md
|
|
27
28
|
- ../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
@@ -162,4 +163,9 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
|
|
|
162
163
|
but not catalog 2. Mechanism:
|
|
163
164
|
[TableView apiWhereClause row-filtering](../../2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md);
|
|
164
165
|
catalog background: [Compass item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
166
|
+
- **Internal-staff nav is its own role (2026-09-28).** Quad now follows the Compass
|
|
167
|
+
`Agilant - Administrators` pattern: a dedicated role carries the 2.5 Supply `SurfaceOverrides`
|
|
168
|
+
nav grants for Items/Vendor Items so Quad's own `@quad.com` users (five of the seven holders of
|
|
169
|
+
role 7 `Global Admin`) do not get them. Migration written, **not yet run**. See
|
|
170
|
+
[Quad internal-admin navigation](./workflows/granting-internal-admin-navigation.md).
|
|
165
171
|
- This profile is a starting point; expand as more Quad-specific behavior is captured.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Giving Quad's internal TOGA staff extra toga25-supply nav (the `Agilant - Administrators` role pattern)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: quad
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-28
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Quad/2026-09-28a - AgilantAdministratorsRoleAndItemsNav.sql
|
|
13
|
+
related:
|
|
14
|
+
- ../profile.md
|
|
15
|
+
- ../../compass-usa/workflows/granting-navigation-access.md
|
|
16
|
+
- ../../../2.0/apps/toga25-supply/features/side-navigation-and-default-route.md
|
|
17
|
+
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
18
|
+
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
How to surface extra 2.5 Supply pages to internal TOGA staff on a tenant without showing them to the client's own users — Quad's first internal-admin role; open for "internal user can't see page X" on any tenant.
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
Internal TOGA staff need pages (Items, Vendor Items, Bundles) that the tenant's own staff must
|
|
26
|
+
**not** see. The house pattern is a **dedicated `Agilant - Administrators` role** that carries the
|
|
27
|
+
nav overrides and nothing else — `Client_Compass` role **9**, `Client_Compasscanada` role **10**,
|
|
28
|
+
and as of 2026-09-28 a new role on `Client_Quad`. Nav gating itself is the shared
|
|
29
|
+
[`SurfaceOverrides IS_VISIBLE` mechanism](../../../2.0/apps/toga25-supply/features/side-navigation-and-default-route.md)
|
|
30
|
+
(element-id map lives there); this doc is only about **which role** gets the override.
|
|
31
|
+
|
|
32
|
+
Never hang an internal nav grant on a shared admin role. `Client_Quad` role **7** (`Global Admin`)
|
|
33
|
+
is held by **7** users, **five of them `@quad.com`** (MNICHOLS, JOSHUAMARTI, DJPUTNEY, mnaber,
|
|
34
|
+
JACEDWARDS) — adding elements 111/112 there would have shown Items and Vendor Items to Quad's own
|
|
35
|
+
staff. That is a tenant-wide behavior change dressed up as a per-user fix.
|
|
36
|
+
|
|
37
|
+
## Steps
|
|
38
|
+
|
|
39
|
+
1. **Check whether the data ACL is already in place** — usually it is, making this nav-only.
|
|
40
|
+
`Client_Quad.AclRecordPermissions` already grants record **19** (`vendor-items`) to role **1**
|
|
41
|
+
(Base), full CRUD, `appId` NULL, and record **21** (`items`) to role **1** at `appId = 1`. So
|
|
42
|
+
every Quad user could already *read* the data; only the menu entry was missing.
|
|
43
|
+
2. **Count the users on the candidate role before granting on it** (`Users_Roles` joined to
|
|
44
|
+
`Users`). Any `@<client-domain>` holder disqualifies the role for an internal-only grant.
|
|
45
|
+
3. **Create a dedicated `Agilant - Administrators` role** in the client DB if the tenant has none,
|
|
46
|
+
and assign only the internal users the developer names. Quad got **`devteam@togatech.com` only**;
|
|
47
|
+
the other three `@togatech.com` Quad users (JWise, NSchenkel, rgreen) were deliberately left off.
|
|
48
|
+
4. **Add role-scoped `SurfaceOverrides` rows** (`surfaceId` 39, the element ids, `IS_VISIBLE` = `'1'`,
|
|
49
|
+
`personaId`/`languageId` NULL) for that new role. Nothing else — no `AclRecordPermissions`, no
|
|
50
|
+
`AclFieldPermissions`, so the migration cannot widen data access.
|
|
51
|
+
5. **Hand the file to the developer to run.** Verify in the app by **logging out and back in** —
|
|
52
|
+
the surface bundle is cached in `localStorage`, so a reload proves nothing.
|
|
53
|
+
|
|
54
|
+
### The 2026-09-28 Quad migration
|
|
55
|
+
`dbchanges2/Client_Quad/2026-09-28a - AgilantAdministratorsRoleAndItemsNav.sql` — committed
|
|
56
|
+
`a890c26` on branch `TRUE-82246`, **not yet run in any environment.** Three guarded statements,
|
|
57
|
+
all inside `Client_Quad`: create the role, assign `devteam@togatech.com`, insert two
|
|
58
|
+
`SurfaceOverrides` rows for elements **111** (`items`) and **112** (`vendor-items`). Core ids
|
|
59
|
+
(39, 111, 112) are **hardcoded literals** — `Core` and `Client_<tenant>` are separate prod
|
|
60
|
+
clusters, so the statement cannot join `Core.SurfaceElements` to resolve them.
|
|
61
|
+
|
|
62
|
+
Quad's starting state for contrast: **one** nav override total (element 109 `sales-orders`, `roleId`
|
|
63
|
+
**NULL** — i.e. tenant-wide) and no internal-admin role at all.
|
|
64
|
+
|
|
65
|
+
## Gotchas
|
|
66
|
+
|
|
67
|
+
- **⚠ `Core.Apps` id 1 = TOGa Supply, id 5 = TOGa Commerce.** Quad's role-7 grant on record 21
|
|
68
|
+
(`items`) is `appId = 5` — **Commerce, not Supply**. It reads like "Global Admin can already see
|
|
69
|
+
Items in Supply" and it cannot. Always read the `appId` on an `AclRecordPermissions` row before
|
|
70
|
+
concluding a role has Supply access.
|
|
71
|
+
- **The internal-admin role may carry zero `AclFieldPermissions`.** `Client_Compass` role 9 has
|
|
72
|
+
**none** — it is a nav/record-scoping role only, and field reads come from the other roles the
|
|
73
|
+
same user holds. An empty field-permission set on this role is correct, not a broken grant.
|
|
74
|
+
- **⚠ These facts were read from a LOCAL (beta/sandbox-derived) copy.** The prod client reader
|
|
75
|
+
`reader1.client.database.togahub.com` was unreachable on 2026-09-28 (`ERROR 2002 … (36)`).
|
|
76
|
+
Re-verify role ids, user lists and existing overrides against production before running the
|
|
77
|
+
migration there.
|
|
78
|
+
|
|
79
|
+
## Change history
|
|
80
|
+
- 2026-09-28 — Created; Quad's first `Agilant - Administrators` role + Items/Vendor Items nav
|
|
81
|
+
overrides written (not yet run), scoped away from `Global Admin` role 7 (apeterson)
|
package/package.json
CHANGED