toga-ai 1.0.924 → 1.0.926

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.
@@ -6,8 +6,8 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [dfranks, bala]
9
+ updated: 2026-10-01
10
+ owners: [dfranks, bala, mhammontree]
11
11
  files:
12
12
  - library/app/framework.php
13
13
  related:
@@ -48,6 +48,7 @@ Two consequences:
48
48
  ## Gotchas
49
49
 
50
50
  - **⚠ A run skipped by the overlap guard leaves NO ROW — absence is ambiguous.** `cronInitialization()` calls `exitIfProcessRunning()` **before** the `CronJobExecutions` INSERT, and the `cronFinished(true)` that guard then calls does nothing because `cronLogId` was never set. So a blocked run is indistinguishable from a run that never launched: **a missing row for an expected slot means "skipped", not "hung"**, and a hung run looks different (it has `dtCheckIn` with no `dtCheckOut`). `isProcessRunning()` matches **any** `ps -ef` line containing the script path (it only excludes `/bin/sh` and its own pid), so a stray `tail -f` or editor on that path blocks the cron indefinitely.
51
+ - **`CronJobExecutions` is huge (~120M rows) with no index on `job`.** Never query it per run (e.g. as a cron's watermark) — it scans the table. Use a small dedicated table instead.
51
52
  - **`db_log` has no `CronJobExecutions` table.** A `cronFinished()` UPDATE that runs against the `db_log` connection throws on **every** 1.0 cron job. Both the INSERT and the UPDATE must target `db_common`.
52
53
  - **A `git merge` can silently resurrect pre-refactor logic.** These functions were cleanly consolidated to a single `db_common`-only INSERT/UPDATE pair (commit `0a3a6f75`), but a later `git merge origin/_production` (`509614036605…`) reintroduced the old dual-path structure from `_production`'s still-unmigrated copy — leaving duplicate INSERT/UPDATE blocks and reverting one path's connection argument back to `db_log`. Automatic merge resolution updated the SQL text (renamed the table) but restored the wrong connection name, producing a platform-wide breaking bug disguised as a simple PR-feedback rework. **When a merge touches a function you already refactored on your branch and the base branch never got that refactor, diff against the merge-base — do not just eyeball the current file** — to catch resurrected logic. (Fixed 2026-07-06: consolidated back to one `db_common` INSERT/UPDATE pair.)
53
54
 
@@ -55,3 +56,6 @@ Two consequences:
55
56
 
56
57
  - [Worker architecture](../../worker/architecture.md), [App_Email queued sending & attachments](./email-queue-attachments.md).
57
58
  - [OneUptime worker2 monitoring](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md), [Tracing a worker cron run in production](../../worker/workflows/tracing-a-worker-cron-run-in-production.md).
59
+
60
+ ## Change history
61
+ - 2026-10-01 — Added the no-index / ~120M-row gotcha (rejected as a watermark source in TRUE-82180). (mhammontree)
@@ -6,8 +6,8 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [jcardinal]
9
+ updated: 2026-10-01
10
+ owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - library/app/framework.php
13
13
  - library/app/frameworkindex.php
@@ -84,9 +84,12 @@ The app depends on the `library` core repo (bootstrapped via `_.php`).
84
84
  ## Gotchas
85
85
 
86
86
  - **Route folders must stay distinct.** Matching is longest-to-shortest, so overlapping route folders can produce surprise matches.
87
- - **An unset `ENVIRONMENT` loads the wrong config file.** `getenv('ENVIRONMENT')` defaults to `'worker'`, so a missing env var silently loads `config.worker.ini`.
87
+ - **An unset `ENVIRONMENT` loads PRODUCTION config.** `getenv('ENVIRONMENT')` defaults to `'worker'`, so running a 1.0 cron locally without it loads `worker/config.worker.ini` — prod DB hosts, `test_mode=0`, `email_errors=1`. Set `ENVIRONMENT` to your dev config name (`config.dev-<name>.ini`) first. Symptom: errors mention `/var/www/cache`.
88
88
  - **Secrets are currently stored in plaintext ini files** — a known practice, not a recommended one. Document where a credential lives, never its value.
89
89
 
90
90
  ## Related
91
91
 
92
92
  - [Library architecture](../architecture.md), [togaview architecture](../../togaview/architecture.md).
93
+
94
+ ## Change history
95
+ - 2026-10-01 — Unset-`ENVIRONMENT` gotcha: it loads production config locally; added symptom and fix (TRUE-82180). (mhammontree)
@@ -15,6 +15,7 @@
15
15
  | [OneUptime Server monitor + disk/memory hygiene on the 1.0 worker EB host](features/oneuptime-server-monitor-host-hygiene.md) | OneUptime Server monitor (per worker role) plus disk/memory hygiene for the legacy AL1 PHP 7.2 1.0 worker EB host; open for the host-metrics agent, monitor crit |
16
16
  | [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Per-minute external OneUptime "Incoming Request" heartbeat from every 1.0 worker box (Degraded 3 min / Offline 5 min); open for the heartbeat push, monitor prov |
17
17
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily 9:00 PM Prudential/Dell cron emailing an Excel of every device shipped today (tracking, serial, XID, FedEx status, vendor); open for the ASN join chain an |
18
+ | [Staples 856 ASN Outbound (NetSuite fulfillments → SFTP, tracked in StaplesAdvanceShipNotices)](features/staples-asn-856-outbound.md) | Hourly cron sending Staples 856 ASNs for shipped NetSuite item fulfillments, one row per fulfillment in `Common.StaplesAdvanceShipNotices`; open for the queue/r |
18
19
  | [Staples cXML Order Import (SFTP → NetSuite)](features/staples-cxml-order-import.md) | Hourly cron importing Staples cXML POs from SFTP into NetSuite Sales Orders (writes 855 ack back); open for the per-file flow, saved-search-8412 Location resolu |
19
20
  | [UMA Student Roster Sync (SFTP → TOGaDeskSupport customers) + Ivanti Ticket Sync](features/uma-student-sync.md) | Daily SFTP full-roster file loads UMA students into TOGaDeskSupport.customers (clientId 10); open for the file format, insert/deactivate rules, how the Ivanti t |
20
21
  | [Walmart Daily Sales Report (send_toga_walmart_report.php)](features/walmart-daily-sales-report.md) | The two Walmart sales workbooks (summary + details) built from `TOGA_Walmart` (in-store) and `TOGA_WalmartEComm` (.COM); open for the pseudo-store 1472 channel |
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Staples 856 ASN Outbound (NetSuite fulfillments → SFTP, tracked in StaplesAdvanceShipNotices)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: staples
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-10-01
10
+ owners: [mhammontree]
11
+ files:
12
+ - worker/crons/sync/staples/staples_asn_netsuite.php
13
+ - dbchanges/Common/MH/2026-09-30.sql
14
+ - worker/schedules/cron.worker.sync.json
15
+ related:
16
+ - ../../../../clients/staples/profile.md
17
+ - ./staples-cxml-order-import.md
18
+ - ../../../../clients/staples/features/oneuptime-sftp-folder-monitors.md
19
+ - ../../library/features/cron-execution-monitoring.md
20
+ - ../../library/features/mvc-page-pattern-and-app-skeleton.md
21
+ ---
22
+
23
+ Hourly cron sending Staples 856 ASNs for shipped NetSuite item fulfillments, one row per fulfillment in `Common.StaplesAdvanceShipNotices`; open for the queue/retry/dedupe design, the 855 gate, statuses, manual SKIPPED, alerts, and how to test it locally.
24
+
25
+ ## Summary
26
+
27
+ `staples_asn_netsuite.php` (`50 * * * *`, `schedules/cron.worker.sync.json`) builds a cXML 856 ASN for each shipped item fulfillment of NetSuite customer **30057** and puts it in Staples SFTP `/out`. Since TRUE-82180 (2026-09-30) every fulfillment is tracked in **`Common.StaplesAdvanceShipNotices`** (`db_common`), so a shipment is retried until sent and never sent twice unless its shipping data changed.
28
+
29
+ **Rule (decided 2026-09-30, Jeff via Mark):** never send Staples the same ASN twice unless the shipping data changed. The fix stays in 1.0 — 2.0 would need an importer rewrite, and NetSuite use will likely move to an internal replacement app later. Old backlog and POs with no 855 are handled by hand, not in code.
30
+
31
+ ## How it works
32
+
33
+ Table: one row per item fulfillment, `UNIQUE itemFulfillmentInternalId`. Status `PENDING` (not sent; `lastError` says why it waits) / `SENT` / `FAILED` (gave up, needs a developer) / `SKIPPED` (manual only, never retried).
34
+
35
+ Each run (`cronInitialization(14400, false)` — must **not** use the read cluster; ends with `cronFinished()` / `closeConnections()`):
36
+
37
+ 1. **Queue.** Search 30057 fulfillments modified in the last 7 days (`ASN_LOOKBACK_SECONDS`). New fulfillment → insert `PENDING`. Existing row whose `dtFulfillmentModified` changed → back to `PENDING` (unless `SKIPPED`).
38
+ 2. **Process** up to 200 (`ASN_MAX_ROWS_PER_RUN`) `PENDING` rows, oldest `dtUpdated` first.
39
+ 3. **855 gate.** A row waits (stays `PENDING`) until `855_<PO>_*` exists in SFTP `/archive`. Likely reason the gate exists: it limits ASNs to orders that came in through the Staples cXML feed (the 856 needs the original cXML in `/archive/archive_in` for Staples line numbers) and keeps 855 before 856. Kept as a wait, not a drop.
40
+ 4. **Dedupe by `shippingHash`** = sha1 of shipping data only (carrier, ship-to, boxes with tracking/weight/items/serials, services). It excludes the random Package identifier, timestamps, and `noticeDate`, so a rebuild of unchanged data gives the same hash. Same hash as last sent → no file. A changed hash re-sends with operation still **`new`** (decided 2026-09-30).
41
+ 5. **Pre-go-live shipments** (`tranDate` before `MIN(dtCreated)` of the table, no hash yet) are marked `SENT` without sending if an old `856_<PO>_*` file in `/archive` or `/out` contains `shipmentID= "<tranId>"` (the old job wrote a space before the quote).
42
+ 6. **Send.** File name `856_<PO>_<fulfillmentInternalId>_<time>`; raw file logged to FileLog job `STAPLES_ASN_OUT_ORDER`. In test mode (`App_Registry::inTestMode()`) the file is written only to the cache folder — never uploaded to the live SFTP.
43
+ 7. **Alerts.** One email (same 3 recipients as before) listing rows not sent after 24 h; each row alerts once (`dtAlerted`, cleared on requeue).
44
+
45
+ **Errors are per row.** A local `set_error_handler` turns PHP warnings into a row failure (otherwise 1.0 `App_Error` exits the whole run). A row fails up to 5 times (`ASN_MAX_SEND_ATTEMPTS`), then `FAILED`. A `SoapFault` / `RuntimeException` (NetSuite or SFTP down) moves the row to the back and stops the run.
46
+
47
+ **Stop checking a PO handled by hand:** `UPDATE StaplesAdvanceShipNotices SET status = 'SKIPPED' WHERE poNumber = '<PO>';`
48
+
49
+ **Deploy order:** merge dbchanges first (`_production` applies on the next worker deploy), then deploy worker.
50
+
51
+ ## Testing locally
52
+
53
+ 1. Load prod `Common` locally and run `dbchanges/Common/MH/2026-09-30.sql`.
54
+ 2. Set `ENVIRONMENT` to your dev config name first (see the unset-`ENVIRONMENT` gotcha in [1.0 MVC page pattern](../../library/features/mvc-page-pattern-and-app-skeleton.md)) — otherwise the run uses production config.
55
+ 3. Run with PHP 7.2 (`xampp_7_2_33`). It reads prod NetSuite and the live SFTP read-only; test mode keeps files in cache.
56
+
57
+ Expected (2026-10-01 run): first run 101 rows, 90 matched as already sent by the old job, 0 files, ~640 s; second run ~100 s, nothing sent; changed data or a new shipment = exactly one file; alert once per row.
58
+
59
+ ## Gotchas
60
+
61
+ - **Old design (pre-TRUE-82180) — why ASNs were late and doubled.** It searched only fulfillments modified in the last 3600 s, so each shipment got one chance; a skipped one went out only when someone edited the fulfillment (days/weeks late, ~half of ASNs). Dedupe checked only `/archive` by prefix `856_<PO>_<n>` where `n` restarted every run, and every fulfillment on the SO was re-sent when one changed.
62
+ - **Do not use `Common.CronJobExecutions` or `Logs.FileLog` as a watermark/dedupe source** — both are too big and unindexed for per-run lookups (see [Cron execution monitoring](../../library/features/cron-execution-monitoring.md) and the FileLog note below). That is why the dedicated table exists.
63
+ - **`Logs.FileLog` has no index on `job` / `fileName`** (verified locally) and `Logs` is ~400 GB — never `LIKE`-scan its BLOB per record; it cannot be pulled locally in full.
64
+ - **Hardcoded credentials (parked).** The SFTP password and the cXML SharedSecret are hardcoded in `staples_asn_netsuite.php`; they should move to config.
65
+
66
+ ## Change history
67
+ - 2026-10-01 — First doc. TRUE-82180 rewrite: `StaplesAdvanceShipNotices` queue with 7-day lookback, shipping-hash dedupe, per-row retry/FAILED, 24 h alert, test mode never uploads, old-job match for pre-go-live shipments. Committed worker `cf5198a27`, dbchanges `8b1f7cb` on branch TRUE-82180; not deployed as of 2026-10-01. (mhammontree)
@@ -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 version" toaster, or `useDeployme |
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-09-25
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. 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
+ 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-09-16
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 and they pick up the new build + data. Implemented as a headless `<DeploymentGuard />` driven by the `useDeploymentGuard` hook, mounted inside the `AuthProvider` / `ToasterProvider` tree in `App.tsx`.
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 the value arriving as an array or a single object). Query: `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` 5 minutes.
26
- 2. **Establish a baseline.** The value observed right after login is stored in `localStorage` key `deployment-version`. The **first** observation only sets the baseline — never logs out. The mismatch check is skipped when no baseline exists.
27
- 3. **Detect a change.** A later fetch whose value differs from the stored baseline triggers the reset.
28
- 4. **Reset UX.** On mismatch: a **non-dismissing info toast** ("A new version is available — signing you out briefly…"), wait **2.5s** for readability, then call `AuthContext` `logout()`.
29
- 5. **Centralized logout does the wipe.** `logout()` runs `performLogout()` + `clearUser()` + `localStorage.clear()` (wipes **all** `localStorage` — tokens, `zu-user`, `zu-hostname`, the persisted query cache, and the `deployment-version` baseline) + `queryClient.clear()`, then `performLogout()` hard-redirects to `/`.
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
- ### Why there is no logout loop
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
- `logout()` clears the baseline and reloads, so the next login's first fetch just re-establishes the baseline (the mismatch check is skipped when no baseline exists). Simpler than the sibling `toga2-commerce` implementation (`src/contexts/AuthContext.tsx`), which polls the same Core parameter but had to preserve the value across logout. toga25-supply's centralized `logout()` (`localStorage.clear` + `queryClient.clear` + `performLogout` redirect) made the port cleaner.
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-09-16
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.
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 30 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 40 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 41 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -4,18 +4,20 @@ framework: "1.0"
4
4
  apps:
5
5
  - worker
6
6
  - worker2
7
+ - dbchanges
7
8
  - dbchanges2
8
9
  project: Worker
9
10
  client: staples
10
11
  type: profile
11
12
  status: active
12
- updated: 2026-09-18
13
+ updated: 2026-10-01
13
14
  owners: [jcardinal, bala, mhammontree]
14
15
  files:
15
16
  - worker/crons/sync/staples/sync_staples_cxml.php
16
17
  - worker/crons/sync/staples/staples_asn_netsuite.php
17
18
  related:
18
19
  - ../../1.0/apps/worker/features/staples-cxml-order-import.md
20
+ - ../../1.0/apps/worker/features/staples-asn-856-outbound.md
19
21
  - ./features/oneuptime-sftp-folder-monitors.md
20
22
  - ../../1.0/apps/worker/features/netsuite-sales-order-sales-rep-sourcing.md
21
23
  ---
@@ -27,7 +29,7 @@ Staples — headless Worker-1.0 integration client (two SFTP crons); open for it
27
29
  Staples is a **headless** integration client — no UI, Worker 1.0 only, depending on the `library` (1.0 `App_`) core. Its entire footprint is two crons under `worker/crons/sync/staples/`, both scheduled in `schedules/cron.worker.sync.json`:
28
30
 
29
31
  1. **`sync_staples_cxml.php`** — `45 * * * *`, "Create Sales Orders in Netsuite From Staples". Inbound: pulls cXML purchase orders off SFTP, creates NetSuite Sales Orders, and writes 855 acknowledgements back. The documented feature — see **`1.0/apps/worker/features/staples-cxml-order-import.md`**.
30
- 2. **`staples_asn_netsuite.php`** — `50 * * * *`, "Create ASN from net suite to staples". Outbound ASN (Advance Ship Notice) from NetSuite back to Staples. (Not yet documented in detail.)
32
+ 2. **`staples_asn_netsuite.php`** — `50 * * * *`, "Create ASN from net suite to staples". Outbound 856 ASN (Advance Ship Notice) per shipped NetSuite fulfillment, tracked in `Common.StaplesAdvanceShipNotices`. Rule: never send the same ASN twice unless shipping data changed; stays in 1.0 (decided 2026-09-30). See **`1.0/apps/worker/features/staples-asn-856-outbound.md`**.
31
33
 
32
34
  **NetSuite identity:** Staples is the NetSuite parent entity/customer **internalId 30057**. Individual Staples buying accounts are NetSuite **child customers** matched by their Staples `CustomerID` against **saved search 8412** (match key `custentity_external_customer_id`; location from `custentity_customer_location`).
33
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.924",
3
+ "version": "1.0.926",
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",