toga-ai 1.0.522 → 1.0.524

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.
@@ -134,11 +134,37 @@ written for a reader and carries no file, line, or trace.
134
134
  ### `Logs.Event.context` shape
135
135
 
136
136
  Identical in both frameworks: **every top-level key holds an object**, and scalars are grouped
137
- under a `Reference` object. Top-level keys are the display names — `Reference`, `GET`, `POST`,
138
- `Runtime`, `AWS`, `SERVER`. `GET`/`POST` are cast to objects so an empty one serializes as `{}`,
139
- not `[]`. `$_POST` is captured and redacted on the same key-name basis as `$_GET` (note PHP only
140
- populates `$_POST` for form encodings, so JSON API bodies deliberately do not appear). The
141
- over-length shed list was renamed in lockstep and its marker is an object too.
137
+ under a `REFERENCE` object. Top-level keys are **UPPERCASE** display names (changed 2026-08-04) —
138
+ `REFERENCE`, `GET`, `POST`, `RUNTIME`, `AWS`, `SERVER`, `GLOBALS`. `GET`/`POST` are cast to objects
139
+ so an empty one serializes as `{}`, not `[]`. `$_POST` is captured and redacted on the same
140
+ key-name basis as `$_GET` (note PHP only populates `$_POST` for form encodings, so JSON API bodies
141
+ deliberately do not appear). The over-length shed list was renamed in lockstep and its marker is an
142
+ object too. Four shapes of this payload coexist for the 7-day retention window — readers keep the
143
+ `REFERENCE` → `Reference` → `reference` fallback chain.
144
+
145
+ ### `$GLOBALS` IS captured (reversal, 2026-08-04) — and 1.0 benefits most
146
+
147
+ `capture.php` mirrors 2.0's `buildGlobalsContext()` / `sanitizeForContext()` /
148
+ `CONTEXT_GLOBALS_TO_EXCLUDE` / `CONTEXT_GLOBALS_MAX_DEPTH` / `CONTEXT_GLOBALS_MAX_STRING_LENGTH`
149
+ exactly. Full rationale, guards, the hostile-fixture results and the **open PII security note**
150
+ live in the
151
+ [2.0 doc](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md#globals-capture--the-earlier-refusal-is-reversed-2026-08-04)
152
+ — read it before touching either copy.
153
+
154
+ Two things are specifically 1.0:
155
+
156
+ - **1.0 crons are the big winner.** They keep almost all working state in globals, so a 1.0 cron's
157
+ captured context is now genuinely rich, where a 2.0 request (state in objects and locals) stays
158
+ thin. The limitation is that `$GLOBALS` reaches **global scope only** — PHP exposes no API for
159
+ another frame's locals; the trace's arguments are the nearest substitute.
160
+ - **`App_Error::redactSensitiveKeys()` is NOT the sanitizer used here.** It is still live on the
161
+ **1.0 Sentry path**, and it redacts only the **top level** of an array and never sees inside an
162
+ object. That weakness is precisely why globals capture was refused before. Do not reach for it
163
+ from capture code, and do not assume it protects anything nested.
164
+
165
+ **PHP 7.2 constraints on the mirror:** no `mixed` type, no named arguments, and no
166
+ `ReflectionProperty::isInitialized()` (7.4+) — `getValue()` is wrapped in its own `try`/`catch`
167
+ instead.
142
168
 
143
169
  ### Elastic Beanstalk environment name on AL1
144
170
 
@@ -182,6 +208,20 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
182
208
 
183
209
  ## Change history
184
210
 
211
+ - 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Mirrored 2.0's reversal:
212
+ `$GLOBALS` is now captured** into a top-level `GLOBALS` group in `capture.php`
213
+ (`buildGlobalsContext()`, `sanitizeForContext()`, `CONTEXT_GLOBALS_TO_EXCLUDE`,
214
+ `CONTEXT_GLOBALS_MAX_DEPTH`, `CONTEXT_GLOBALS_MAX_STRING_LENGTH`), byte-compatible with
215
+ `_underscore/Error.php`. 1.0 crons benefit most because they keep working state in globals — which
216
+ is why Sentry's Globals section was so much richer than ours. Recorded that
217
+ `App_Error::redactSensitiveKeys()` (still live on the 1.0 Sentry path) redacts only the top level
218
+ of an array and never enters an object, which is exactly why capture was refused before the
219
+ Reflection-based sanitizer existed. Added `dsn`/`connection`/`passphrase`/`salt` to
220
+ `CONTEXT_SENSITIVE_KEY_FRAGMENTS` (a DSN embeds `user:password`). Top-level context keys are now
221
+ **UPPERCASE**. PHP 7.2 sweep on this copy: no `mixed`, no named arguments, no
222
+ `ReflectionProperty::isInitialized()` — `getValue()` is wrapped in its own try/catch. The **open
223
+ PII exposure** this widens is documented on the 2.0 doc; `library` and `tools` must both deploy
224
+ before any of it is observable. (jcardinal)
185
225
  - 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Fixed: 1.0 errors recorded
186
226
  `clientId` NULL on every cron but one** (2,093 NULL `Logs.Event` rows vs 107 with a client; issue
187
227
  `1Q` from `crons/toga2/quad/import_po_and_asn.php`). `App_Api_Toga2::authenticate()` — 1.0's real
@@ -107,6 +107,17 @@ statement about *response time*, not about the error, and nothing on the page le
107
107
  those apart. **Nothing renders while the clock is running but under 24h** — below that the clock
108
108
  contributes nothing to urgency, so saying "neglect" would overstate it.
109
109
 
110
+ **The note must render on ONE line** (fixed 2026-08-04). `.neglect-note` was
111
+ `display: inline-block`, so "neglect NORMAL · 26h" broke after "neglect" and the level dropped to a
112
+ second line — the two halves then read as unrelated fragments. It is now `display: block` +
113
+ `white-space: nowrap`.
114
+
115
+ > **⚠ Widening the column is PART of that fix, not a nicety.** Under `table-layout: fixed`, a
116
+ > `nowrap` element in a column too narrow to hold it **overflows the cell** rather than shrinking —
117
+ > so `nowrap` alone would have traded a wrap for an overflow. `col-urgency` went **96px → 136px**
118
+ > and `.issue-listing` `min-width` **1026px → 1066px** to match. Any future `nowrap` in this table
119
+ > needs the same paired column-width change.
120
+
110
121
  **Deliberately scoped to the neglect contribution only.** The console never claims *which* axis is
111
122
  winning: computing the full driving axis in Tools would mean copying the volume, sustained and
112
123
  release-band thresholds too, and that much duplicated policy would rot.
@@ -209,15 +220,34 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
209
220
  - **The distinct-EB-environment query is a full scan of `Event`.** Acceptable only because that
210
221
  table is bounded by the 7-day purge. At volume the proper fix is denormalising the EB environment
211
222
  onto its own `Event` column **at capture time**.
212
- - **Read both the old and new `Event.context` key positions.** The context shape was restructured
213
- on 2026-08-04 (one object per top-level key, scalars under `Reference`); with 7-day retention,
214
- both shapes coexist for a week after any such change.
223
+ - **⚠ FOUR shapes of `Event.context` coexist — every reader must tolerate all of them.** The shape
224
+ was restructured on 2026-08-04 (one object per top-level key, scalars grouped) and the top-level
225
+ keys were then **uppercased** (`Reference` → `REFERENCE`, `Runtime` → `RUNTIME`). With 7-day
226
+ retention, older shapes stay in the table for a week after each change. `issue/get.php` therefore
227
+ reads **`'REFERENCE'` first, then `'Reference'`, then `'reference'`**, and the JSON-tree
228
+ open-by-default test is **case-insensitive** (`strtolower($key) === 'reference'`) so the reference
229
+ card stays expanded for both casings. The key humanizer needed no change — it already passes
230
+ all-caps keys through untouched. **This fallback-chain pattern is the rule for any context key
231
+ rename, not a one-off.** A new `GLOBALS` group also now appears on newer events (see the
232
+ [2.0 doc](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md)) — it is the
233
+ largest group by far, so the largest-first card ordering will usually put it on top.
215
234
  - **Curated text is authoritative.** Once `isManaged` is set, later occurrences no longer update
216
235
  subject/description, so a stale curated subject stays stale until someone edits it again.
217
236
 
218
237
  ## Change history
219
238
 
220
- - 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Built:** a **neglect
239
+ - 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Fixed:** the neglect
240
+ indicator wrapped onto two lines in the listing — `.neglect-note` was `display: inline-block`, so
241
+ "neglect NORMAL · 26h" broke after "neglect" and read as two unrelated fragments. Now
242
+ `display: block` + `white-space: nowrap`, **with `col-urgency` 96px → 136px and `.issue-listing`
243
+ min-width 1026px → 1066px as part of the same fix**: under `table-layout: fixed` a `nowrap`
244
+ element in a too-narrow column overflows the cell instead of shrinking, so nowrap alone would have
245
+ traded a wrap for an overflow. **Changed:** capture now emits **UPPERCASE** top-level
246
+ `Event.context` keys, so `issue/get.php` reads `'REFERENCE'` → `'Reference'` → `'reference'` and
247
+ its JSON-tree open-by-default test became case-insensitive; **four shapes of the payload now
248
+ coexist** across the 7-day retention window and every reader must tolerate them. A new `GLOBALS`
249
+ context group also starts appearing on newer events. (jcardinal)
250
+ - 2026-08-04 (earlier) — **Built:** a **neglect
221
251
  indicator** on both pages — listing gained an `openTaskAgeHours` correlated subquery and a
222
252
  `$describeNeglect` amber note (`.neglect-note`, `#8a5a00`) under the urgency badge; the detail
223
253
  page's *Acknowledged* row now reads "not yet — waiting 3 days" with a NORMAL/HIGH neglect chip.
@@ -177,18 +177,98 @@ read the ambient value.
177
177
 
178
178
  ### `Event.context` shape (uniform, both frameworks)
179
179
 
180
- **Every top-level key holds an object**; scalars are grouped under a `Reference` object. The
181
- top-level keys *are* the display names: `Reference`, `GET`, `POST`, `Runtime`, `AWS`, `SERVER`.
180
+ **Every top-level key holds an object**; scalars are grouped under a `REFERENCE` object.
181
+ **Top-level keys are UPPERCASE** (changed 2026-08-04): `REFERENCE`, `GET`, `POST`, `RUNTIME`,
182
+ `AWS`, `SERVER`, `GLOBALS`. `GET`/`POST`/`AWS`/`SERVER` already were; `Reference` → `REFERENCE`
183
+ and `Runtime` → `RUNTIME` bring the rest into line. These are **group headings, not data**, and
184
+ the mixed casing read as though the difference meant something.
185
+
182
186
  `GET`/`POST` are cast to objects so an empty one serializes as `{}` rather than `[]`. `$_POST` is
183
- now captured (redacted on the same key-name basis as `$_GET`; PHP only populates `$_POST` for form
187
+ captured (redacted on the same key-name basis as `$_GET`; PHP only populates `$_POST` for form
184
188
  encodings, so JSON API bodies deliberately do not appear). The duplicate top-level
185
189
  `peakMemoryMb` was dropped, the over-length shed list was renamed in lockstep with its marker now
186
190
  an object, and 2.0 gained `framework` / `isBusiness` / `issueKey` / `errorFile` / `errorLine` for
187
191
  1.0 parity.
188
192
 
189
193
  A uniform shape is what lets a viewer render context **generically** (see the Tools console's
190
- card/JSON-tree renderer). Consumers must read **both** the old and new key positions during the
191
- 7-day `Event` retention overlap.
194
+ card/JSON-tree renderer).
195
+
196
+ > **⚠ FOUR shapes of this payload now coexist in `Event`.** Retention is 7 days, so every reader
197
+ > must tolerate the older ones until they purge. Tools reads `'REFERENCE'` first and falls back to
198
+ > `'Reference'` then `'reference'`; its JSON-tree open-by-default test is **case-insensitive**
199
+ > (`strtolower($key) === 'reference'`) so the reference card stays expanded for every shape. The
200
+ > Tools key humanizer already passes all-caps keys through untouched. **Never rename a context key
201
+ > without leaving the fallback chain in place.**
202
+
203
+ ### `GLOBALS` capture — the earlier refusal is REVERSED (2026-08-04)
204
+
205
+ **`$GLOBALS` IS now captured**, into a new top-level `GLOBALS` group, in **both** frameworks
206
+ (`_underscore/Error.php` `buildGlobalsContext()`; mirrored in `library/app/error/capture.php`).
207
+
208
+ > This **reverses** the earlier decision on this page that `$GLOBALS` would deliberately never be
209
+ > captured. That refusal was correct *for the sanitizer that existed at the time* — a key-name
210
+ > blocklist that recursed **arrays only** and walked straight past framework singletons holding DB
211
+ > credentials in **object properties**. That rationale is now the reason **the sanitizer had to be
212
+ > rewritten**, not a reason to refuse capture.
213
+
214
+ **Why reversed:** our context was compared against Sentry's "Globals" section for the same error
215
+ and carried none of it. The variable that explains a failure is routinely the one nobody thought
216
+ to allowlist in advance, and 1.0 crons in particular keep almost all working state in globals —
217
+ which is exactly why the Sentry payload was so much richer.
218
+
219
+ **What made it safe enough:** `sanitizeForContext()` **recurses into objects via Reflection,
220
+ including PRIVATE and PROTECTED properties**, and tests every key at every depth.
221
+ **Reflection, not `get_object_vars()`** — a credential is far likelier to be private than public,
222
+ and *a sanitizer that cannot see a value cannot redact it*. (1.0's older
223
+ `App_Error::redactSensitiveKeys()`, still live on the 1.0 Sentry path, redacts only the **top
224
+ level** of an array and never looks inside an object. That is the old behaviour, not this one.)
225
+
226
+ Guards:
227
+
228
+ - `CONTEXT_GLOBALS_MAX_DEPTH = 6` — an object graph with parent pointers must not walk a whole
229
+ schema into a log row.
230
+ - `CONTEXT_GLOBALS_MAX_STRING_LENGTH = 2000` per string, with the **true byte length appended**.
231
+ - `CONTEXT_GLOBALS_TO_EXCLUDE` — the superglobals (already their own groups) plus
232
+ `GLOBALS`, `appRoot`, `newAppRoot`, `argv`, `argc`.
233
+ - `Closure`s and resources render as placeholders.
234
+ - **`GLOBALS` is shed FIRST by `encodeContext()`** when the payload exceeds
235
+ `CONTEXT_MAX_LENGTH` (60000), so an oversized dump costs itself and never the curated groups.
236
+
237
+ Verified against a deliberately hostile fixture: a private `$dbPassword` nested inside an object
238
+ property → `[REDACTED]`; `apiSecret` three levels deep in an array → `[REDACTED]`;
239
+ `shippingAddress1`/`shippingPhone`/`shippingEmail` → `[REDACTED]`; a 5000-char string truncated; a
240
+ resource and a `Closure` rendered safely.
241
+
242
+ > **Known limitation — GLOBAL SCOPE ONLY.** A variable local to the function that threw is *not*
243
+ > reachable from a handler; PHP exposes no API for another frame's locals. The stack trace's
244
+ > arguments are the nearest substitute, and the trace already carries them. Practical effect: rich
245
+ > capture for **1.0 crons**, comparatively thin for a **2.0 request** where state lives in objects
246
+ > and locals.
247
+
248
+ #### ⚠ SECURITY — residual PII that key-name redaction cannot catch (open, not solved)
249
+
250
+ `Event.context` is a table **every client shares**, and its contents also flow into **ClickUp task
251
+ bodies and escalation emails**. Key-name redaction cannot recognise PII **by value**, so from a
252
+ real Compass MITS payload these are still stored in clear: `shippingName`, `shippingAttention`
253
+ (person and company names), `shippingCity` / `shippingStateCode` / `shippingZip` (the rest of an
254
+ address whose *line 1* is redacted), and `mitsClientId` (an `odp_pk_`-style publishable-looking
255
+ key that no blocklist flags).
256
+
257
+ This is the **same exposure that was deliberately closed earlier the same day** by dropping the
258
+ `print_r($payload)` dump from
259
+ `worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php` — reopened
260
+ through a wider door, knowingly, at the developer's direction. Sentry was an acceptable home for
261
+ this data because it is access-controlled, single-tenant and not mirrored into tickets;
262
+ `Logs.Event` is **none of those**.
263
+
264
+ Three mitigations were offered and **not yet applied** (follow-ups):
265
+
266
+ 1. Extend the sensitive-fragment list (`city`, `zip`, `postal`, `attention`, `recipient`).
267
+ 2. Add **value-pattern** redaction — things that look like emails, phone numbers and card numbers
268
+ regardless of key name.
269
+ 3. Exclude named globals per-cron via `CONTEXT_GLOBALS_TO_EXCLUDE`.
270
+
271
+ **Recommended: 2 + 1.**
192
272
 
193
273
  ### The quotable reference — base-32 over an unambiguous alphabet (reworked 2026-08-04)
194
274
 
@@ -320,12 +400,22 @@ in `_underscore` and `library` in the **same** release, and the `/errors` consol
320
400
  form. **Evaluation order matters:** in `IssueAreaOwners`, `consecutiveCount` is assigned
321
401
  **before** `clickupUserId` so the `IF()` compares against the *old* owner. Reversing them
322
402
  makes the condition always true and the counter never resets.
323
- - **`Events.context` is an allowlist, never a dump.** It replaced `print_r($GLOBALS)` filtered
324
- by a key-name blocklist — that blocklist recursed **arrays only** and walked straight past
325
- objects whose *properties* hold DB credentials, into a table shared by every client.
326
- `requestUri` and `referer` have query strings stripped (a `?token=abc` was `[REDACTED]` under
327
- `get` and stored in clear in `requestUri`). The sensitive-fragment list covers **PII**, not
328
- just credentials. Persisted traces are rebuilt **argument-free**.
403
+ - **`Event.context` is a curated allowlist PLUS a separately-sanitized `GLOBALS` group** (as of
404
+ 2026-08-04 — see the GLOBALS section above; the earlier "never capture `$GLOBALS`" position is
405
+ **superseded**). The original `print_r($GLOBALS)` dump it replaced was filtered by a key-name
406
+ blocklist that recursed **arrays only** and walked straight past objects whose *properties* hold
407
+ DB credentials — that is why capture was refused until `sanitizeForContext()` gained Reflection
408
+ descent into private/protected properties. `requestUri` and `referer` have query strings stripped
409
+ (a `?token=abc` was `[REDACTED]` under `get` and stored in clear in `requestUri`). The
410
+ sensitive-fragment list covers **PII**, not just credentials — but only **by key name**, so
411
+ value-recognisable PII still gets through (see the security note above). Persisted traces are
412
+ rebuilt **argument-free**.
413
+ - **`CONTEXT_SENSITIVE_KEY_FRAGMENTS` must include `dsn` and `connection`.** Added 2026-08-04
414
+ alongside `passphrase` and `salt`, in **both** frameworks. A property named `dsn` holding
415
+ `mysql:host=…` came through in clear on the hostile fixture — **a DSN embeds `user:password`**.
416
+ It had never mattered before because under the allowlist-only context no global ever reached the
417
+ redactor at all. Any new fragment must be added to `_underscore/Error.php` **and**
418
+ `library/app/error/capture.php` together.
329
419
  - **Keep the debug `print_r($GLOBALS)` dump inside the `isDebugMode()` branch.** It was being
330
420
  built on every exception — including the OOM path the memory reserve exists to protect.
331
421
  - **Strict-mysqli varchar overflow silently drops the row.** `_Error::initialize()` sets
@@ -430,6 +520,31 @@ clientUserId). **Neither was built.** As built instead:
430
520
 
431
521
  ## Change history
432
522
 
523
+ - 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **DECISION REVERSED:
524
+ `$GLOBALS` IS now captured**, as a new top-level `GLOBALS` group, in both frameworks. The earlier
525
+ entry recording that `$GLOBALS` would deliberately never be captured is **superseded and has been
526
+ corrected in place** — its rationale (a key-name blocklist recurses arrays only and steps past
527
+ framework singletons holding DB credentials in object properties) is now recorded as *the reason
528
+ the sanitizer had to be rewritten*. Reversed because our context was compared against Sentry's
529
+ "Globals" section and carried none of it; the variable that explains a failure is routinely one
530
+ nobody thought to allowlist, and 1.0 crons keep almost all state in globals. Made safe by a new
531
+ `sanitizeForContext()` that **recurses into objects via Reflection including private/protected
532
+ properties** (not `get_object_vars()` — a credential is likelier private, and a sanitizer that
533
+ cannot see a value cannot redact it), with depth cap 6, a 2000-char string cap that appends the
534
+ true byte length, superglobals + `GLOBALS`/`appRoot`/`newAppRoot`/`argv`/`argc` excluded,
535
+ Closure/resource placeholders, and `GLOBALS` **shed first** by `encodeContext()` past
536
+ `CONTEXT_MAX_LENGTH`. Verified against a hostile fixture. **Known limitation: global scope only** —
537
+ PHP exposes no API for another frame's locals, so 2.0 requests capture comparatively little.
538
+ **Fixed:** `dsn`, `connection`, `passphrase`, `salt` added to `CONTEXT_SENSITIVE_KEY_FRAGMENTS` in
539
+ both frameworks — a `dsn` property came through in clear on the fixture and a DSN embeds
540
+ `user:password`. **Changed:** every top-level `Event.context` key is now **UPPERCASE**
541
+ (`Reference`→`REFERENCE`, `Runtime`→`RUNTIME`); readers must tolerate **four** coexisting shapes
542
+ for the 7-day retention window. **Recorded as an OPEN security note (not solved):** key-name
543
+ redaction cannot recognise PII by value, so `shippingName`/`shippingAttention`/`shippingCity`/
544
+ `shippingStateCode`/`shippingZip`/`mitsClientId` are still stored into a table every client shares
545
+ and mirrored into ClickUp bodies and escalation emails — the same exposure closed earlier the same
546
+ day in the Compass MITS transmit cron, knowingly reopened at the developer's direction. Three
547
+ mitigations offered, none applied. (jcardinal)
433
548
  - 2026-08-04 — **Correction/diagnostic:** recorded that the `dbchanges2/Logs/` migration files lag
434
549
  the deployed schema — `2026-07-30a` describes a `code` identity column, but live `Logs.Issue` is
435
550
  keyed on **`issueKey` (varchar 128)** — and listed the live column set. Query
@@ -9,4 +9,4 @@
9
9
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
10
10
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
11
11
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
12
- | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/src/utils/getFontAwesomeIcon.tsx |
12
+ | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
@@ -6,10 +6,11 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-23
9
+ updated: 2026-08-04
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga-blox/.github/workflows/publish.yml
13
+ - toga-blox/package.json
13
14
  - toga-blox/src/utils/getFontAwesomeIcon.tsx
14
15
  related:
15
16
  - ../architecture.md
@@ -99,11 +100,36 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
99
100
  `React.CSSProperties & Record<\`--${string}\`, string>` — the value type must be `string`
100
101
  (**not** `string | number`; `CSSVariables` requires `string`). Runtime and function signature
101
102
  are unchanged, so it is safe for the ~40 consumers.
103
+ - **The `_production`/`_beta` build copies only `.scss` — a plain `.css` component
104
+ stylesheet is silently dropped from the tarball.** On `_production` and `_beta` the
105
+ `build` script is `tsc && copyfiles -u 1 "src/**/*.scss" dist`: `tsc` compiles a
106
+ component's `import "./Foo.css"` into the emitted JS, but `copyfiles` never ships the
107
+ `.css`, so the published package contains JS importing a file that isn't in the tarball.
108
+ Consumers then fail their Vite/Rollup build with `Could not resolve "./Foo.css"`
109
+ (seen with `EnvironmentBadge` in `@agilant/toga-blox@beta` 1.0.316-beta.108, breaking
110
+ `toga2-commerce`). **Convention on the prod/beta build: author component stylesheets as
111
+ `.scss`, never `.css`** (a flat `.css` file is valid SCSS as-is, so renaming is the
112
+ zero-risk fix — no build-script change). Note the build description in `architecture.md`
113
+ (copies `*.scss/*.css/*.module.css`) reflects the in-progress `feature-new-table` branch,
114
+ **not** the current `_production`/`_beta` build; that feature branch is not to be merged
115
+ to prod/beta just to pick up the wider copy glob.
116
+ - **Diagnose what a channel actually shipped with `npm pack`.** To see exactly which files a
117
+ published dist-tag contains, run `npm pack @agilant/toga-blox@<tag>` then
118
+ `tar tzf <tarball>` — this is how the missing `.css` (JS importing an unshipped stylesheet)
119
+ was confirmed against the tarball rather than the source tree.
102
120
  - **Secret hygiene done right.** `publish.yml` writes a transient `.npmrc` at build time from
103
121
  GitHub secrets (`${NODE_AUTH_TOKEN}` / FontAwesome token) — it never commits tokens. This is
104
122
  the pattern the consuming apps should follow (see the commerce workflow's secret-hygiene note).
105
123
 
106
124
  ## Change history
125
+ - 2026-08-04 — Recorded the prod/beta `.scss`-only build gotcha: the `_production`/`_beta`
126
+ `build` copies only `src/**/*.scss`, so a component shipped with a plain `.css` + matching
127
+ `import "./X.css"` publishes JS referencing a stylesheet absent from the tarball, breaking
128
+ consumers' Vite build (`Could not resolve "./EnvironmentBadge.css"`, `@agilant/toga-blox@beta`
129
+ 1.0.316-beta.108 → `toga2-commerce`). Fix was renaming `EnvironmentBadge.css`→`.scss` (no
130
+ build-script change), republished on `_beta` as 1.0.316-beta.115 and a `fix/env-badge-scss-prod`
131
+ PR into `_production`; `toga2-commerce` `_beta` bumped to the fixed version. Added the
132
+ `npm pack`/`tar tzf` tarball-inspection diagnostic. (apeterson)
107
133
  - 2026-07-23 — Documented CI version derivation (strip suffix → patch+1 → append `-<mode>.<run>`;
108
134
  dev owns only the base version, never the suffix) and the `_sandbox-client` deploy procedure now
109
135
  driven by the `deploy-sandbox-client` project skill. Recorded that `publish.yml` has diverged
@@ -348,6 +348,14 @@ this controller needs the same treatment.**
348
348
 
349
349
  ## Gotchas / known issues
350
350
 
351
+ - **⚠ This cron is an EXFILTRATION PATH for `Event.context` — treat anything captured as
352
+ ticket-visible and email-visible.** As of 2026-08-04 capture writes a `GLOBALS` group into
353
+ `Event.context`, and redaction is **by key name only**, so value-recognisable PII (names, city,
354
+ state, ZIP, publishable-looking ids) survives into ClickUp task bodies and escalation emails —
355
+ neither of which is single-tenant or access-controlled the way Sentry was. Before widening what
356
+ this cron copies out of `context`, read the security note in the
357
+ [2.0 error-reporting doc](../../_underscore/features/error-reporting-issue-event.md). Mitigations
358
+ are proposed but **not applied**.
351
359
  - **An issue held at HIGH purely by neglect never de-escalates on its own.**
352
360
  `applyDeEscalationDwell()` needs the computed target to sit **below** the stored urgency for
353
361
  `REQUIRED_QUIET_RUNS` (3) consecutive runs, but the neglect axis keeps recomputing the target
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.522",
3
+ "version": "1.0.524",
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",