toga-ai 1.0.924 → 1.0.925
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 +1 -1
- package/knowledge/2.0/apps/toga2-commerce/features/force-logout-on-deployment.md +38 -5
- package/knowledge/2.0/apps/toga25-supply/features/force-logout-on-deployment.md +13 -10
- package/knowledge/2.0/standards/frontend.md +10 -1
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
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
|
|
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, "why wasn't I logged out?", a repeating "new versi |
|
|
17
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 |
|
|
18
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 |
|
|
19
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 |
|
|
@@ -6,13 +6,13 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-01
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/src/hooks/useDeploymentGuard.tsx
|
|
13
13
|
- toga2-commerce/src/lib/deploymentVersion.ts
|
|
14
|
+
- toga2-commerce/src/lib/queryClient.ts
|
|
14
15
|
- toga2-commerce/src/components/AuthLayout/AuthLayout.tsx
|
|
15
|
-
- toga2-commerce/src/contexts/AuthContext.tsx
|
|
16
16
|
- toga2-commerce/src/lib/authStorage.ts
|
|
17
17
|
related:
|
|
18
18
|
- ./sso-redirect-and-session-gating.md
|
|
@@ -20,7 +20,7 @@ related:
|
|
|
20
20
|
- ../../toga25-supply/features/force-logout-on-deployment.md
|
|
21
21
|
---
|
|
22
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`.
|
|
23
|
+
Signs every user out when a deploy bumps a Core parameter, after showing a notice; open for surprise logouts, "why wasn't I logged out?", a repeating "new version" toaster, or `useDeploymentGuard`.
|
|
24
24
|
|
|
25
25
|
## Summary
|
|
26
26
|
|
|
@@ -30,17 +30,50 @@ Ported from toga25-supply (`force-logout-on-deployment`). Two commerce-only diff
|
|
|
30
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
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
32
|
|
|
33
|
+
Reached commerce `_production` only in merge `26e2fae7` (2026-09-30, TRUE-81991); supply had it earlier. That gap is why one parameter bump logged supply users out and commerce users not at all.
|
|
34
|
+
|
|
33
35
|
## How it works
|
|
34
36
|
|
|
35
|
-
1.
|
|
36
|
-
2. First observation writes the baseline and never signs out.
|
|
37
|
+
1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` **5 minutes**.
|
|
38
|
+
2. First observation writes the baseline (`action: "establish-baseline"`) and never signs out.
|
|
37
39
|
3. A later differing value → toaster → 2.5s timer → `logout()` + reload.
|
|
38
40
|
4. `logout()` clears `AUTH_STORAGE_KEYS`, including the baseline, so the next login just re-baselines.
|
|
39
41
|
|
|
42
|
+
### What actually triggers a refetch (and what does not)
|
|
43
|
+
|
|
44
|
+
Exactly three things, verified 2026-10-01:
|
|
45
|
+
|
|
46
|
+
1. query mount — a full page load or browser refresh;
|
|
47
|
+
2. `refetchOnWindowFocus` — see the gotcha below for what "focus" really means;
|
|
48
|
+
3. the 5-minute `refetchInterval`.
|
|
49
|
+
|
|
50
|
+
**An in-app route change does not refetch.** Neither app invalidates `["deploymentVersion"]` on `location.pathname`, and supply has no broad `invalidateQueries()` anywhere. "It logged me out when I changed pages" is always (2) or (3) landing at that moment.
|
|
51
|
+
|
|
52
|
+
`refetchIntervalInBackground` is unset (default `false`), so the 5-minute poll **pauses while the tab is hidden** and resumes on return. Nobody is signed out in a background tab.
|
|
53
|
+
|
|
54
|
+
**User-visible guarantee:** a signed-in user is signed out within 5 minutes of the bump while their tab is open and on screen; if the tab was hidden, at the moment they come back.
|
|
55
|
+
|
|
56
|
+
### Query config vs supply (does not change sign-out timing)
|
|
57
|
+
|
|
58
|
+
| | app-wide `refetchOnWindowFocus` | guard query |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| toga25-supply (`App.tsx`) | default `true` — every on-screen query refetches on focus | inherits |
|
|
61
|
+
| toga2-commerce (`lib/queryClient.ts`) | `false` | opts back in individually |
|
|
62
|
+
|
|
63
|
+
Because the guard opts in, sign-out timing is **identical** in both apps. Deleting commerce's `refetchOnWindowFocus: false` buys no change in logout behavior and costs a burst of requests on every tab switch. Commerce `origin/_production` and `origin/_sandbox-client` are byte-identical for `useDeploymentGuard.tsx`, `App.tsx`, and `queryClient.ts` — sandbox-vs-prod differences were timing, not build.
|
|
64
|
+
|
|
40
65
|
## Gotchas
|
|
41
66
|
|
|
67
|
+
- **⚠ "Window focus" means tab visibility, not app focus.** React Query v5 (`query-core` 5.101.4) registers **only** `visibilitychange`; the v4 focus/blur listeners are gone. Alt-tabbing to Teams/Outlook and back does **not** refetch — the tab was never hidden. Switching browser tabs, minimize/restore, and lock/wake do. Applies to every `refetchOnWindowFocus` query in every 2.0 React app. See [frontend standard §4](../../../standards/frontend.md).
|
|
68
|
+
- **⚠ The first bump after the guard ships is always a no-op.** `localStorage["deployment-version"]` is written in one place only — the guard's own first observation — and nothing seeds it at login. A browser that has never run the guard has nothing to compare against, so the first fetch just stores the value. By design (it is the no-logout-loop guard), but it means "we bumped the parameter and nobody was logged out" is expected right after a deploy that introduces the guard.
|
|
69
|
+
- **⚠ `key=` on `GET /core-parameters` is silently ignored.** Both apps send `?fields=value&key=META_LAST_REFRESH_DATETIME`. api2 parses only `fields, where, join, ojoin, sort, recordsPerPage, page, distinct, group, client, seekValues, seekUuid, surface`, and `/core-parameters` is plain CRUD (no scripted method on `Model/Core/Parameter.php`). The response is page 1 of **all** Core Parameters and the code reads `coreParameters[0].value`. It works today only because row 0 happens to be the right one — adding or reordering Core Parameters would silently point the guard at another row. Correct form: `where: { and: [{ key: { "=": "META_LAST_REFRESH_DATETIME" } }] }`. Never rely on `key=` as a filter anywhere in 2.0.
|
|
42
70
|
- **⚠ 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
71
|
- **⚠ 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
72
|
|
|
73
|
+
## Deferred — sign out on route change
|
|
74
|
+
|
|
75
|
+
To also fire on an in-app navigation: a `useLocation()` effect calling `refetch()` on `pathname` change, with `refetch` held in a **ref** (its identity changes every render; depending on it directly loops). Commerce can do this as-is — `<DeploymentGuard />` is inside `AuthLayout`, inside the router. **Supply cannot**: its guard sits in `App.tsx` **above** `<AppRouter />`, so `useLocation()` would throw; the guard must move inside the router first. Not implemented — deferred 2026-10-01.
|
|
76
|
+
|
|
45
77
|
## Change history
|
|
78
|
+
- 2026-10-01 — Documented the exact refetch triggers (mount / tab-visibility / 5-min poll; **not** route change), the background-pause behavior, the first-bump no-op, the ignored `key=` param, and the commerce-vs-supply query config difference. Route-change refetch deferred. (apeterson)
|
|
46
79
|
- 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)
|
|
@@ -6,28 +6,31 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-01
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/hooks/useDeploymentGuard.tsx
|
|
13
13
|
- toga25-supply/src/App.tsx
|
|
14
|
-
related:
|
|
14
|
+
related:
|
|
15
|
+
- ../../toga2-commerce/features/force-logout-on-deployment.md
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
Force-signs-out every user when a deploy bumps a Core parameter, so session + caches reset to the new build; open when debugging surprise logouts or `useDeploymentGuard`.
|
|
18
19
|
|
|
19
20
|
## Summary
|
|
20
21
|
|
|
21
|
-
On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`. The FE watches it and, when it changes mid-session, force-signs-out every logged-in user so session, `localStorage`, and React Query cache (including the persisted `supply-chain-query-cache`) reset cleanly
|
|
22
|
+
On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`. The FE watches it and, when it changes mid-session, force-signs-out every logged-in user so session, `localStorage`, and React Query cache (including the persisted `supply-chain-query-cache`) reset cleanly. Implemented as a headless `<DeploymentGuard />` driven by `useDeploymentGuard`, mounted inside the `AuthProvider` / `ToasterProvider` tree in `App.tsx` — **above** `<AppRouter />`, so the hook cannot use `useLocation()`.
|
|
22
23
|
|
|
23
24
|
## How it works
|
|
24
25
|
|
|
25
|
-
1. **Poll the Core parameter.** TanStack `useQuery` reads `META_LAST_REFRESH_DATETIME` via `apiGet("/core-parameters", { fields: ["value"], key: "META_LAST_REFRESH_DATETIME" })`, reading `response.data.coreParameters` (tolerant of
|
|
26
|
-
2. **Establish a baseline.** The
|
|
27
|
-
3. **Detect a change.** A later fetch whose value differs
|
|
28
|
-
4. **Reset UX.**
|
|
29
|
-
5. **Centralized logout does the wipe.** `logout()` runs `performLogout()` + `clearUser()` + `localStorage.clear()` (
|
|
26
|
+
1. **Poll the Core parameter.** TanStack `useQuery` reads `META_LAST_REFRESH_DATETIME` via `apiGet("/core-parameters", { fields: ["value"], key: "META_LAST_REFRESH_DATETIME" })`, reading `response.data.coreParameters` (tolerant of an array or a single object). Query: `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` 5 minutes.
|
|
27
|
+
2. **Establish a baseline.** The first observed value is stored in `localStorage["deployment-version"]`; the first observation only sets the baseline and never logs out.
|
|
28
|
+
3. **Detect a change.** A later fetch whose value differs triggers the reset.
|
|
29
|
+
4. **Reset UX.** Non-dismissing info toast ("A new version is available — signing you out briefly…"), wait **2.5s**, then `AuthContext` `logout()`.
|
|
30
|
+
5. **Centralized logout does the wipe.** `logout()` runs `performLogout()` + `clearUser()` + `localStorage.clear()` (tokens, `zu-user`, `zu-hostname`, the persisted query cache, and the `deployment-version` baseline) + `queryClient.clear()`, then hard-redirects to `/`. Clearing the baseline is what prevents a logout loop.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
**When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*). Supply-only difference: `App.tsx` leaves `refetchOnWindowFocus` at React Query's default `true` app-wide, so every on-screen query refetches on tab visibility — not just this one. Sign-out timing is the same in both apps.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## Change history
|
|
35
|
+
- 2026-10-01 — Recorded that the guard sits above the router (blocks `useLocation()`); moved the shared trigger/gotcha detail to the commerce doc rather than duplicating it. (apeterson)
|
|
36
|
+
- 2026-09-16 — Initial implementation. (apeterson)
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-10-01
|
|
9
9
|
owners: [jcardinal, apeterson]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -191,6 +191,15 @@ const queryClient = new QueryClient({
|
|
|
191
191
|
});
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
**MUST: read `refetchOnWindowFocus` as "tab became visible", not "app regained focus".**
|
|
195
|
+
React Query v5 (`query-core` 5.101.4) registers **only** `visibilitychange`; v4's focus/blur
|
|
196
|
+
listeners are gone. So alt-tabbing to another **application** (Teams, Outlook) and back does
|
|
197
|
+
**not** refetch — the browser tab was never hidden. Switching browser **tabs**,
|
|
198
|
+
minimize/restore, and lock/wake do. Likewise `refetchInterval` **pauses while the tab is
|
|
199
|
+
hidden** unless `refetchIntervalInBackground: true` is set. Do not design a freshness or
|
|
200
|
+
session guarantee on the assumption that alt-tab refetches.
|
|
201
|
+
(Case: [commerce force-logout-on-deployment](../apps/toga2-commerce/features/force-logout-on-deployment.md).)
|
|
202
|
+
|
|
194
203
|
**SHOULD: fetch with React Query, not react-router data APIs.** Both apps **deliberately avoid**
|
|
195
204
|
router `loader`/`action`/`useLoaderData`; data flows through React Query so caching, invalidation,
|
|
196
205
|
and optimistic updates live in one system. Do not mix the two paradigms.
|
package/package.json
CHANGED