toga-ai 1.0.496 → 1.0.498

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,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-01
9
+ updated: 2026-08-03
10
10
  owners: ["dfranks", "jcardinal"]
11
11
  files:
12
12
  - _underscore/Error.php
@@ -87,6 +87,28 @@ produced a different hash on every request — dedup never actually worked.
87
87
  cannot orphan a curated Issue: an admin repoints the new fingerprint at the existing Issue
88
88
  from the Tools console to merge them.
89
89
 
90
+ ### Where each exception class routes (the class you throw IS the routing decision)
91
+
92
+ | Thrown class | HTTP | Sentry | `Logs.Issue` | Fingerprint / routing |
93
+ |---|---|---|---|---|
94
+ | `_Exception_Validation` | **400** | no | **no** | n/a — bad client input. Still leaves a full `Logs.Api` row (see [api2 request logging](../../api2/features/request-logging.md)) |
95
+ | `_Exception_Business` | **500** | yes | **yes** | stable `issueKey` (refactor-immune) → business recipients configured in the Tools console |
96
+ | plain global `Exception` | **500** | yes | **yes** | **trace**-based fingerprint (moves when code moves) → developer ClickUp task |
97
+ | `_Exception` | — | — | — | **never throw it deliberately** (below) |
98
+
99
+ Verified against `api2/Controller/Index.php` (≈L205-245, L369-377) and `_Error.php`
100
+ (`captureException` ≈L222-265, `buildFingerprint` ≈L455-490) on **2026-08-03**, while TRUE-78188
101
+ was still in flight — **re-verify against `Controller/Index.php`** rather than trusting the table
102
+ blindly.
103
+
104
+ **`_Exception` must never appear at a deliberate throw site.** Its constructor takes **five**
105
+ required args (`errorNumber`, `errorString`, `errorFile`, `errorLine`, `trace`) because it is an
106
+ *error-handler shape* populated by `_Error` from `set_error_handler` context — not a general
107
+ exception. `throw new _Exception('msg')` raises an `ArgumentCountError` **before** the intended
108
+ exception is ever constructed, so the real signal is destroyed. Jeff states this rationale in the
109
+ `Exception/Business.php` docblock. Census in `_underscore`: 173 plain `throw new Exception` vs 14
110
+ `_Exception`.
111
+
90
112
  ### `_Exception_Business` — identity immune to refactoring
91
113
 
92
114
  `_underscore/Exception/Business.php` carries a stable `issueKey` plus a `minimumUrgency` floor.
@@ -267,6 +289,12 @@ clientUserId). **Neither was built.** As built instead:
267
289
 
268
290
  ## Change history
269
291
 
292
+ - 2026-08-03 — Added the **exception-class routing table** (`_Exception_Validation` → 400/no
293
+ Issue; `_Exception_Business` → 500 + `issueKey`-fingerprinted Issue to business recipients;
294
+ plain `Exception` → 500 + trace-fingerprinted Issue to a developer task) and recorded that
295
+ **`_Exception` must never be thrown deliberately** — its 5-arg error-handler constructor raises
296
+ `ArgumentCountError` first. Verified against `Controller/Index.php` / `_Error.php` while
297
+ TRUE-78188 was still in flight. (dfranks)
270
298
  - 2026-08-01 — **Decided: `Logs` cluster tables use SINGULAR names.** Renamed all six
271
299
  post-deployment (`Issues`→`Issue`, `Events`→`Event`, `IssueFingerprints`→`IssueFingerprint`,
272
300
  `IssueClickupTasks`→`IssueClickupTask`, `IssueEmailAddresses`→`IssueEmailAddress`,
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-28
10
- owners: [jcardinal, bala, mhammontree]
9
+ updated: 2026-08-03
10
+ owners: [jcardinal, bala, mhammontree, dfranks]
11
11
  files:
12
12
  - api2/Controller/Index.php
13
13
  - api2/Component/Api/V2/V2.php
@@ -102,6 +102,12 @@ One ~2,000-line `execute()` then `processRoutePairs()`:
102
102
  `public`, `delegator`/`encrypted`. JWTs HS256, `sub` access/refresh (access 3600s,
103
103
  refresh 30d); signing secret **rotated** with current+previous accepted across the
104
104
  boundary. The JWT `id` claim carries the full identity used for ACL.
105
+ **`/v2/auth/oauth` credential transport:** OAuth 2.0 `client_credentials`. `client_id` +
106
+ `client_secret` may arrive as **HTTP Basic** *or* as POST body fields — **Basic takes
107
+ priority** when both are present. `scope` is **required**; `grant_type` is **optional** and
108
+ defaults to `client_credentials` (accepted: `client_credentials`, `password`,
109
+ `refresh_token`). The call mints a JWT; subsequent requests carry `Authorization: Bearer`.
110
+ (`api2/Component/Api/V2/V2.php` L414-460.)
105
111
  4. **Response envelope** — `{transactionId, timestamp, authority, audience, isSuccess,
106
112
  status, error, messages[], meta{}, data{}}`; `isSuccess` = 2xx status.
107
113
  5. **Transaction logging** — every request logged (to client/core Logs DB, or as a JSONL
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-29
9
+ updated: 2026-08-03
10
10
  owners: ["mhammontree", "dfranks"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -46,6 +46,21 @@ In `api2/Component/Api/V2/V2.php` (~L2168) every request is logged and routed:
46
46
  - There is also a **config-gated branch** (~L2166): when `_Config::api('log_filepath')` is set,
47
47
  the log entry is written to a **file** on the host instead of the DB.
48
48
 
49
+ ### A rejected request (HTTP 400) is NOT unlogged
50
+
51
+ `api2/Controller/Index.php` (L369-377) handles a non-success response by **committing** the
52
+ `DB_LOGS` and `DB_CLIENT_LOGS` transactions and rolling back **only** the business-data
53
+ transaction. A rejection therefore still writes a **full `Client_Logs.Api` / `Core_Logs.Api`
54
+ row** — request payload, response payload, `apiId`, `sourceIp`, `transactionId` — while the
55
+ business row is discarded.
56
+
57
+ So a 400 **is** SQL-queryable and attributable to the calling credential. What a 400 lacks is a
58
+ `Logs.Issue` row: **no alerting, no aggregation, no owner** (see
59
+ [Error Reporting — Issue/Event Capture](../../_underscore/features/error-reporting-issue-event.md)).
60
+ That is the deciding factor when choosing between `_Exception_Validation` (400, logged as a
61
+ request, no Issue) and `_Exception_Business` (500, Sentry + a routed Issue). Do **not** reach for
62
+ `_Exception_Business` merely to "make sure the rejection is recorded" — it already is.
63
+
49
64
  ## Blind spots when auditing a client's API traffic
50
65
 
51
66
  Looking only at `Logs_<Client>.Api` misses four sources:
@@ -95,6 +110,11 @@ re-send.
95
110
 
96
111
  ## Change history
97
112
 
113
+ - 2026-08-03 — Corrected a common wrong assumption: an **HTTP 400 leaves a full log row**.
114
+ `Controller/Index.php` L369-377 commits `DB_LOGS`/`DB_CLIENT_LOGS` and rolls back only the
115
+ business transaction, so rejections are queryable and attributable to the credential; what they
116
+ lack is a `Logs.Issue` (no alerting/owner) — the deciding factor between `_Exception_Validation`
117
+ and `_Exception_Business`. (dfranks)
98
118
  - 2026-07-29 — Documented (TRUE-80179) that the core/writer `Logs.Api` retains the **full original
99
119
  `requestPayload`** on failed writes, making it a **replayable backlog source** and a scope
100
120
  diagnostic: dedupe to one-per-contract with a window function on
@@ -5,8 +5,8 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-08-01
9
- owners: [jcardinal, mhammontree]
8
+ updated: 2026-08-03
9
+ owners: [jcardinal, mhammontree, dfranks]
10
10
  files: []
11
11
  related:
12
12
  - ../apps/_underscore/architecture.md
@@ -573,15 +573,42 @@ $c = 'Hello ' . $world;
573
573
 
574
574
  * Always use exceptions to handle unexpected conditions.
575
575
 
576
- ### Interceptor / model client-error rejections → throw `_Exception_Validation`, not `_Exception`
577
-
578
- A hard-block raised inside a pre/post interceptor (or model op) to reject a **bad client request**
579
- must throw **`_Exception_Validation`** (`_underscore/Exception/Validation.php`), which
580
- `api2/Controller/Index.php` (≈lines 204–222) maps to `DEFINED_MESSAGE_ERROR_BAD_REQUEST` →
581
- **HTTP 400** with the message carried in the response envelope (so the front end can drive a
582
- toaster). Throwing a plain `_Exception` yields an **HTTP 500 + stack trace** — wrong for a
583
- client-input rejection and an information leak. First applied: the three Rate WH hard-blocks
584
- (missing / invalid / duplicate service address), TRUE-79533.
576
+ ### Choosing an exception class is a ROUTING decision, not style
577
+
578
+ At a deliberate throw site the class you pick determines the HTTP status, whether Sentry sees
579
+ it, whether a `Logs.Issue` is created, how that Issue is fingerprinted, and **who gets
580
+ notified**. Pick deliberately:
581
+
582
+ | Throw this | When | HTTP | Sentry | `Logs.Issue` | Goes to |
583
+ |---|---|---|---|---|---|
584
+ | `_Exception_Validation` | bad **client input** — a hard-block in a pre/post interceptor or model op | **400** | no | no | the caller, via the response envelope (front end can toast it) |
585
+ | `_Exception_Business` | a **business condition a business user can act on and a developer cannot fix** | 500 | yes | yes, keyed by a stable `issueKey` | business recipients configured in the Tools `/errors` console |
586
+ | plain global `Exception` | a genuine **code defect / unexpected state** | 500 | yes | yes, fingerprinted by **trace** | a developer ClickUp task |
587
+
588
+ `_Exception_Business`'s constructor is `(issueKey, message, minimumUrgency)`. The `issueKey` is
589
+ the fingerprint, so the Issue's identity survives refactoring; a plain `Exception`'s
590
+ trace-based fingerprint changes when the code moves.
591
+
592
+ **A 400 is not "unlogged."** `api2/Controller/Index.php` commits the log transactions and rolls
593
+ back only the business transaction, so a rejection still writes a full `Logs.Api` row (request
594
+ + response payload, `apiId`, `sourceIp`, `transactionId`). What a 400 lacks is a `Logs.Issue` —
595
+ no alerting, no aggregation, no owner. Do not upgrade a client-input rejection to
596
+ `_Exception_Business` just to "make sure it's recorded."
597
+
598
+ ### NEVER `throw new _Exception(...)`
599
+
600
+ `_Exception` is an **error-handler shape**, not a general exception: its constructor requires
601
+ five arguments (`errorNumber`, `errorString`, `errorFile`, `errorLine`, `trace`) because
602
+ `_Error` populates it from `set_error_handler` context. `throw new _Exception('msg')` raises an
603
+ `ArgumentCountError` **before** your exception is ever constructed, destroying the real signal.
604
+ Throwing a plain `_Exception` also yields an HTTP 500 + stack trace — wrong for a client-input
605
+ rejection and an information leak. Census in `_underscore`: 173 plain `throw new Exception` vs
606
+ 14 `_Exception`.
607
+
608
+ Sources: `_underscore/Exception.php`, `Exception/Validation.php`, `Exception/Business.php`,
609
+ `_underscore/Error.php`, `api2/Controller/Index.php`. First applied: the three Rate WH
610
+ hard-blocks (TRUE-79533). Taxonomy verified 2026-08-03 **while TRUE-78188 was still in flight** —
611
+ re-verify against `Controller/Index.php` before relying on it.
585
612
 
586
613
  ### Comprehensive Error Handling
587
614
 
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-13
10
- owners: ["jcardinal", "rgirish"]
9
+ updated: 2026-08-03
10
+ owners: ["jcardinal", "rgirish", "dfranks"]
11
11
  files:
12
12
  - _underscore/Model/Compass/PurchaseOrder.php
13
13
  - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
@@ -29,6 +29,56 @@ production data-integrity bug.
29
29
  - USA/Canada subclasses (`Model/Compass/Usa|Canada/PurchaseOrder.php`) are empty and inherit
30
30
  this behavior — edit the parent.
31
31
 
32
+ ## Who actually calls `POST /v2/purchase-orders` (MITS is NOT the only caller)
33
+
34
+ `/v2/purchase-orders` for Compass is a **shared endpoint with two unrelated inbound channels**,
35
+ distinguished in `Core.ClientApiIdentities` (clientId 2). **The channels are told apart by the
36
+ FIELD NAME, not by whether a MITS SO is present at all:**
37
+
38
+ | Identity | Channel | SO field sent | Values | `purchaseOrderItems` |
39
+ |---|---|---|---|---|
40
+ | `153531108` | MITS | **`mitsSalesOrder`** (unprefixed) | only `SA…` or `MR…` | always present |
41
+ | `COMPASS-CXML` | cXML / EDI | **`c_mitsSalesOrder`** (`c_`-prefixed) | numeric, e.g. `472981379001` | present |
42
+ | `COMPASS-CXML` | cXML / EDI | **neither field** | — | often **absent entirely** |
43
+
44
+ Measured over 60 days of inbound `POST /v2/purchase-orders` (`Logs_Compass.Api`):
45
+ - **MITS — 4,547 posts, 100% carry the unprefixed `mitsSalesOrder`**; `SA` 2,928, `MR` 1,619.
46
+ Never `MA`, never any other prefix. **No `SA` value has ever been sent by a non-MITS caller.**
47
+ - **COMPASS-CXML — 2,697 posts**: 2,455 carry `c_mitsSalesOrder`, and **242 carry neither field**.
48
+ In a 30-day slice: 1,508 `c_mitsSalesOrder` posts all had items; **136 posts had neither field
49
+ and no `purchaseOrderItems` key** — the legitimate header-first EDI POs.
50
+ - Compass **Canada** posts under a **separate identity (`409531`)**, so never hardcode an
51
+ identity id.
52
+
53
+ **Consequences for any validation added to this endpoint:**
54
+ - **The unprefixed `mitsSalesOrder` is the MITS marker** — not the identity id, and not the
55
+ `SA`/`MR` prefix. Supporting evidence: `prePost` reads `$payload->mitsSalesOrder`; the only
56
+ `c_mitsSalesOrder` occurrence in the codebase is the model field declaration at
57
+ `_underscore/Model/Compass/PurchaseOrder.php:18`; and `Core.ApiPayloadInterceptors` has **no
58
+ rows for `recordId = 17`** (the `purchase-orders` record), so nothing rewrites field names in
59
+ flight.
60
+ - A "reject purchase orders with zero line items" rule keyed on the **unprefixed** field cannot
61
+ touch the 136 item-less EDI posts — they carry neither field. Gating on **item count alone**
62
+ would reject all of them.
63
+ - **`MA` orders never arrive from MITS.** Compass creates them manually; they go to ODP, reach us
64
+ over EDI, and land on an exception report for manual NetSuite entry. (An earlier belief that MA
65
+ is a third MITS prefix to filter on is wrong.)
66
+
67
+ > **⚠ Querying trap — a substring match on `mitsSalesOrder` conflates the two callers**, because
68
+ > `c_mitsSalesOrder` *contains* it. Anyone characterising this traffic from `Logs_Compass.Api`
69
+ > must match the **exact key**; a pattern like `"mitsSalesOrder":"` also silently misses the
70
+ > prefixed form, which is how this session first reached the wrong conclusion that cXML never
71
+ > sends an SO reference. Second trap: payloads appear **both minified**
72
+ > (`"mitsSalesOrder":"MR…"`) **and pretty-printed** (`"mitsSalesOrder": "MR…"`, space after the
73
+ > colon), so a naive pattern under-counts a second way.
74
+
75
+ > **OPEN / UNVERIFIED:** it is **not** confirmed whether the 2.0 framework strips the `c_` prefix
76
+ > when populating `$payload` for an interceptor. If it does, cXML requests carrying
77
+ > `c_mitsSalesOrder` would also enter a guard keyed on `$payload->mitsSalesOrder`. Harmless while
78
+ > those posts all carry items, but a future item-less cXML post carrying `c_mitsSalesOrder` would
79
+ > then be wrongly rejected. This **cannot be settled from logs** — it needs a dev-environment
80
+ > check.
81
+
32
82
  ## How it works
33
83
  - **SA orders (normal):** each inbound PO item carries `createdFromSalesOrderItem.lineNumber`
34
84
  (the MITS line, which includes configuration-line offsets). `prePost` remaps it to the real
@@ -50,6 +100,17 @@ production data-integrity bug.
50
100
  because SA/MR originate in Compass (their Compass SO+items pre-exist) whereas MA is
51
101
  ODP-originated, so nothing else would populate it.
52
102
 
103
+ ### MR and SA consume PO line items differently (asymmetry inside one method)
104
+ - **MR** — `handleMrOrder` (≈L54-57) **silently skips** any item whose
105
+ `vendorItem->item->partNumber` is empty while building `salesOrderItems`. So an MR PO whose
106
+ lines *all* lack a `partNumber` passes a naive `count($payload->purchaseOrderItems) > 0` check
107
+ and still creates an **empty Sales Order**. A "usable item" filter for MR must therefore test
108
+ `partNumber`, not array length.
109
+ - **SA** — items are linked via `createdFromSalesOrderItem` →
110
+ `salesOrderItemPurchaseOrderItems`, and the quantity cross-check (≈L318-324) explicitly
111
+ `continue`s past `partNumber`-less lines rather than discarding them. Applying the MR
112
+ `partNumber` filter to SA would **reject valid orders**.
113
+
53
114
  ## Data model
54
115
  - `SalesOrderItems_PurchaseOrderItems` (`Client_Compass`): `uuid`, `salesOrderItemId`,
55
116
  `purchaseOrderItemId`. One row per (SO item, PO item) the PO fulfills.
@@ -59,6 +120,15 @@ production data-integrity bug.
59
120
  USA and Canada share the parent handler unchanged.
60
121
 
61
122
  ## Gotchas / known issues
123
+ - **SQL injection in `prePost` (OPEN as of 2026-08-03).** `_underscore/Model/Compass/PurchaseOrder.php`
124
+ **L232** interpolates the raw client payload straight into SQL:
125
+ `WHERE SalesOrders.number LIKE '{$payload->mitsSalesOrder}'` — no escape, no cast. The **same
126
+ value is escaped at L301**, so the file is internally inconsistent and the correct pattern is
127
+ already present. Fix with `_Database::escape()` per the 2.0 back-end standard. Reachable only by
128
+ a caller holding valid Compass API credentials (OAuth2 `client_credentials`), so it is **not**
129
+ anonymous or internet-facing — normal priority, not an emergency. Separately, five sites in this
130
+ file (L148, L301, L461, L462, L471) use **`addslashes()`** where the standard requires
131
+ `_Database::escape()`.
62
132
  - **Spurious cross-part links from the `postPost` lineNumber join (fixed 2026-06-11).** The
63
133
  `postPost` link pass matched `SalesOrderItems.lineNumber = PurchaseOrderItems.lineNumber`
64
134
  for *all* orders. For SA orders that is wrong: PO line numbers are PO-local, so whenever an
@@ -137,6 +207,16 @@ USA and Canada share the parent handler unchanged.
137
207
  need item backfill from the sibling ODP SO first).
138
208
 
139
209
  ## Change history
210
+ - 2026-08-03 — Documented that `/v2/purchase-orders` has **two** Compass inbound channels (MITS
211
+ `153531108` vs `COMPASS-CXML`; Canada is a third identity `409531`), told apart by **field
212
+ name**: unprefixed `mitsSalesOrder` = MITS, `c_mitsSalesOrder` = cXML, neither = the item-less
213
+ header-first EDI POs. So a zero-item rejection keyed on the unprefixed field is safe, while
214
+ gating on item count alone is not. Recorded the substring/pretty-print querying traps that
215
+ conflate the two callers, and left open whether the framework strips the `c_` prefix for
216
+ interceptor payloads. Also recorded the MR-vs-SA `partNumber` item-consumption
217
+ asymmetry (MR silently skips `partNumber`-less lines → empty SO despite `count() > 0`) and an
218
+ open SQL-injection at L232 plus five `addslashes()` sites. Corrects the earlier belief that
219
+ `MA` arrives from MITS. (dfranks)
140
220
  - 2026-07-13 — Fixed empty MA orders: the 3a ODP-PO-import cron's new-SO branch now builds
141
221
  `salesOrderItems` from the 850 PO items (the MA back-linking block excludes MA by design).
142
222
  Forward-only; 192 historical empties still need item backfill. (rgirish)
@@ -0,0 +1,88 @@
1
+ ---
2
+ type: session
3
+ slug: toga25-loading-states
4
+ title: Design-mockup loading states for toga25-supply tables and modals
5
+ author: tcox
6
+ repos: [toga25-supply, toga-blox-npm]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-08-03
11
+ updated: 2026-08-03
12
+ ---
13
+
14
+ # Session: toga25-loading-states
15
+ **Date:** 2026-08-03
16
+ **Project/Repo:** toga25-supply + toga-blox-npm (2.0)
17
+ **Task:** Implement the "Toga25-supply mockup" design's loading states — table skeletons (initial/refresh/filter), pagination loading, Refresh busy pill, bulk progress toast, and modal skeletons (cold/warm/sub-section) — pixel-faithful to the mockup, in toga25-supply with supporting toga-blox changes.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+ - **TableSkeleton built on the real table's CSS module** — `src/components/TableSkeleton/TableSkeleton.tsx` imports `@agilant/toga-blox/dist/components/Table/themeConfig/toga.module.css` (the exact module the blox PrimaryTable is styled by), so skeleton and loaded table are pixel-identical by construction. Real header labels + static sort glyphs from table meta, type-aware cells (status dot, avatar circle, image tile + label bar, badge pill), real pagination chrome. **User visually approved.** Wired into `PrimaryTableServerLayout` replacing blox `TableShimmer`.
23
+ - **Shimmer texture matched to design** — `Shimmer.module.css` overlay rewritten to the mockup gradient: 100deg, transparent→38%, `#F7F8FB` at 50% (strong: `#F0F2F6`), →62% transparent, 100vw-wide band, 2s linear. Fixes every skeleton app-wide (modals included).
24
+ - **Pagination loading** — blox `TablePagination` gained `loadingPage?: number | null` (threaded through `PrimaryTable` + server template + both types); shows 12px spinner + `Loading page N…` (`.paginationLoadingText`, #72757D/14px) in place of the count; `TableBody` dims to 0.55 (`.bodyDimmed`) while a page fetch is in flight; controls stay full-strength (pointer-events blocked only). App wrapper detects page fetch via new `page` prop vs `paginationMeta.page` (`isPageFetch`) — refresh/filter/sort still show the full skeleton (per design), page changes keep rows.
25
+ - **Refresh busy pill** — all 5 pages (SalesOrders, Items, Bundles, VendorItems, Inventory): icon spins 1.1s in fixed 20px box (`.refreshIconBox` in `src/index.css`), label flips to `Refreshing…`; `useIsFetching({queryKey:["table-data"], predicate: q => q.state.data !== undefined})` excludes cold loads.
26
+ - **Fidelity audit (agent) against mockup source** confirmed shimmer/spinner/copy exact and produced 14 fixes, all applied: no columnBorders on skeleton (loaded table has none), rows 16, sort glyph placeholder, avatar/image/badge shapes, active-filter chip stays in skeleton header (`filteredColumnSlugs` from `columnFilters`), bulk toast track 14px + `background-attachment: fixed` + cancel spacing.
27
+ - **Modal skeletons (sales-order modal)** — cold = existing `SalesOrderModalSkeleton` (header shimmer); warm = new `warmHeader` prop renders real `SalesOrderTopBar` from the clicked row while body shimmers; row context plumbed as optional `row` on both wrappers' `renderModal` (server + client), passed as `rowRecord` from SalesOrders.tsx. Sub-section refresh = `SubSectionShimmer` (3×36px r6 bars) in Invoices/Shipments sections while their independent queries (`useInvoices`/`useShipments`) load — sections keep frame+title, no longer pop in late; `getDetailSections` show-gates include loading.
28
+ - **CodeRabbit fixes** — client wrapper's `renderModal` moved OUTSIDE the loading gate (open modal no longer unmounts on table refetch); shared `TableRowRecord = { uuid: string } & Record<string, unknown>` exported from `PrimaryTableServerLayout/types.ts`, reused in both wrappers + modal, `displayRecords` tightened to it (killed 2 pre-existing no-explicit-any errors).
29
+ - **Components added**: `BulkProgressToast` (design-exact, unwired — no bulk feature exists), `SubSectionShimmer`; barrel exports updated.
30
+ - **Client testing infra** — hosts entries added via UAC-elevated PowerShell (`Start-Process powershell -Verb RunAs`) for `compasscanada.dev.sandbox.togasupply` and `nychh.dev.sandbox.togasupply`; NYCHH is the inventory client (only one with dedicated `NYCHH_INVENTORY_GROUPINGS`). All dev servers closed at session end.
31
+ - Every round verified with `npx tsc --noEmit` + eslint (touched files) + `npm run build` — all clean at session end.
32
+
33
+ ## What did NOT work — DO NOT RETRY THESE
34
+ - **Hand-rolled skeleton from mockup geometry + big-bang parallel implementation.** First attempt (3 parallel agents: components + table wiring + modal wiring incl. error states) rendered a skeleton with no card border, no header labels, equal-width bars, bright white glint — user: "further off than we started" and **reverted everything** (`git checkout` + clean). Do not rebuild loading UI from the mockup's px values directly; mirror the app's real rendered table (shared CSS module) and ship ONE state at a time for visual sign-off.
35
+ - **`background-attachment: fixed` for the unison shimmer inside modals** — breaks inside the record modal's transform (treated as local). Use the per-element 100vw translateX overlay with the same gradient instead (deliberate, documented in `Shimmer.module.css`). It IS fine for `BulkProgressToast`'s progress fill (not inside a transform).
36
+ - **Rebuilding toga-blox while dev servers run** — `prebuild rm -rf dist` fails with "Directory not empty" (Windows file locks from Vite dep-optimizer). Stop all vite servers first, then `cd /c/WWW/toga-blox-npm && npm run build`, then restart (and `rm -rf node_modules/.vite` in the app if the browser shows stale-dep errors).
37
+ - **Starting a client dev server without its hosts entry** — vite resolves `--host` via DNS at startup: `Error: getaddrinfo ENOTFOUND compasscanada.dev.sandbox.togasupply`. The hosts entry must exist BEFORE `npm run <client>`. Binding 127.0.0.1 instead is NOT a workaround: the client slug comes from the first hostname segment at login.
38
+ - **Background Bash tasks inherit `c:\WWW` cwd, not the repo** — `npm run compasscanada` failed "Missing script"; always `cd /c/WWW/toga25-supply && npm run …` in background commands. Foreground shell cwd also resets between some calls.
39
+ - **`TaskStop` on an `npm run` vite server orphans the node child** holding the port — after stopping, kill listeners: `netstat -ano | grep LISTENING | grep ":517"` → `taskkill //F //PID <pid>`.
40
+ - **Blanket sed for `page,` insertion** hit the wrong block in `useItemFulfillmentModalViewModel.tsx` (duplicated `page` inside a destructure = syntax error). Targeted Edit with unique context fixed it.
41
+ - **`useIsFetching` without a predicate** made Refresh show "Refreshing…" during cold loads (design shows it only for refetches of on-screen data).
42
+
43
+ ## Not tried yet (candidates for next session)
44
+ - **Table error states** (server/timeout/empty/no-results/permission/maintenance/ratelimit per mockup ErrorBlock) — spec extracted at scratchpad `table-states-spec.md` §2.12-2.14/§4; first attempt reverted; needs the app-side error surface (`useTableData` discards query errors — an app hook subscribing to the query cache by `["table-data", slug]` prefix worked in the reverted round and can be reused conceptually).
45
+ - **Modal error states** (generic/not-found/permission/conflict per `modal-states-spec.md` §§7-10) — `src/components/RecordLoadError` exists (unused, generic copy matches mockup); six placeholder `<>Error</>` sites mapped in the reverted round's integration map.
46
+ - **Warm open for Item/VendorItem/Bundle record modals** — same two-step as sales-order: accept `rowRecord`, hand real header to skeleton via `warmHeader`-style prop.
47
+ - **TableSkeleton for `PrimaryTableClientLayout`** (modal-embedded tables still use blox `TableShimmer`; also its skin is `supply-modal-table`, different tokens).
48
+ - **Bulk feature wiring** — `BulkProgressToast` has zero consumers; per-row processing scrim (100deg `rgba(63,93,202,0.06)` at 50%) not implemented; app has no bulk selection at all.
49
+ - **Class-B inherited chrome differences (USER DECISION PENDING)** — app table vs mockup: header ~38px/14px vs 34px/13px, rows ~40px vs 48px, cell padding 25px vs 12px, separators #D4D5D8 vs #E8E9EA, "per page" vs "rows shown", filter chip #4f6bd4/r2 vs #3F5DCA/r5 (looks like a blox bug), card shadow. Fixing = restyling the live table in toga-blox.
50
+ - **blox bug**: `.tableAndPaginationInner.supply` sets `font-size: var(--primaryTable-pagination-text-default)` which resolves to a COLOR (#8C8E96) — declaration silently dropped, text inherits ~16px.
51
+
52
+ ## Current file state
53
+ | File | Status | Notes |
54
+ |------|--------|-------|
55
+ | toga25-supply `src/components/TableSkeleton/` | new | CSS-module-mirroring table skeleton (approved) |
56
+ | toga25-supply `src/components/BulkProgressToast/` | new | design-exact, unwired |
57
+ | toga25-supply `src/components/SubSectionShimmer/` | new | 3×36px bars sub-section well |
58
+ | toga25-supply `src/components/Shimmer/Shimmer.module.css` | modified | 100deg subtle band per design |
59
+ | toga25-supply `src/components/index.ts` | modified | new barrel exports |
60
+ | toga25-supply `src/layout/PrimaryTableServerLayout/{.tsx,types.ts}` | modified | TableSkeleton gate, `page`/isPageFetch, `loadingPage`, `filteredColumnSlugs`, `renderModal.row`, `TableRowRecord` |
61
+ | toga25-supply `src/layout/PrimaryTableClientLayout/{.tsx,types.ts}` | modified | `renderModal` outside gate + row context; still blox TableShimmer for loading |
62
+ | toga25-supply 6 table layouts + 2 VMs | modified | pass `page` through (SalesOrders/Items/Bundles/VendorItems/Generic/ItemFulfillment) |
63
+ | toga25-supply 5 pages | modified | Refresh busy pill + predicate |
64
+ | toga25-supply `src/index.css` | modified | `.refreshIconBox` + 1.1s spin keyframes |
65
+ | toga25-supply SalesOrderRecordModalLayout + view/VM/sections/skeleton/helpers | modified | warm/cold/sub-section modal loading states |
66
+ | toga-blox-npm (branch `feature-new-table`) `TablePagination.tsx` + `types.ts` | modified | loadingPage indicator, full-strength controls |
67
+ | toga-blox-npm `PrimaryTable.tsx` + `Table/types.ts` | modified | loadingPage threading, bodyDimmed on TableBody |
68
+ | toga-blox-npm `toga.module.css` | modified | `.paginationSpinner`, `.paginationLoadingText`, `.bodyDimmed` |
69
+ | toga-blox-npm `dist/` | rebuilt | current with all source changes |
70
+ | **Both repos: all changes UNCOMMITTED** (toga25-supply on `phase-two`) | — | user decides when/how to land |
71
+
72
+ ## Decisions made
73
+ - **Skeletons mirror the real rendered app table, not the mockup's raw geometry** — share the blox CSS module so load→loaded never shifts; the mockup's own px values only apply where the app matches them. Chosen after the hand-rolled attempt was reverted. Rejected: patching blox `TableShimmer` (8 hardcoded columns, Tailwind-JIT-fragile).
74
+ - **One state at a time with user visual sign-off** — replaced the 3-slice parallel big-bang that got reverted.
75
+ - **Per-element translateX shimmer overlay** (viewport-anchored gradient breaks in transformed modals); exact mockup gradient stops preserved.
76
+ - **Page-fetch detection at the wrapper** via requested `page` vs `paginationMeta.page` — no blox useTableData changes needed.
77
+ - **Loading-state-only scope** — explicitly did NOT change Refresh button idle chrome (`secondaryActionAlt` stays, though mockup pill is blue), nor any class-B live-table styling; user instruction.
78
+ - **`TableRowRecord` typed with `unknown` not `any`**; alias lives in the server layout types (the "table-layout contract"), client types re-export.
79
+ - **Modal-embedded client tables render bare when empty** (no onboarding panel) — empty is normal there.
80
+
81
+ ## Blockers
82
+ none — pending items are user decisions (class-B chrome; when to commit) and the next feature slices.
83
+
84
+ ## Exact next step
85
+ > With DevTools throttling on, visually verify the three modal skeleton states in the sales-order modal (click a row = warm header; reload with the uuid in the URL = cold; watch Invoices/Shipments shimmer wells), then replicate the warm pattern to ItemRecordModalLayout, VendorItemRecordModalLayout, and BundleRecordModalLayout, and start the table error states from `table-states-spec.md` (scratchpad specs may be gone — re-extract from `Toga25-supply mockup/src/inventory-table.jsx` §ErrorBlock if so).
86
+
87
+ ---
88
+ _Saved by /session-save on 2026-08-03_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.496",
3
+ "version": "1.0.498",
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",