toga-ai 1.0.1069 → 1.0.1071

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,11 +6,13 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-18
10
- owners: [apeterson]
9
+ updated: 2026-10-17
10
+ owners: [apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
13
13
  - _underscore/Model/Compass/SalesOrder.php
14
+ - _underscore/Model/Client/Certification.php
15
+ - _underscore/Model/Client/UserCertification.php
14
16
  - _underscore/Model.php
15
17
  related:
16
18
  - sales-order-status-filter-surface.md
@@ -34,7 +36,10 @@ Before this, `filterOptions` in table meta was filled in **only** for `TYPE__STA
34
36
  column whose options are **live data** (e.g. the list of active Admin users): seeding a surface
35
37
  element per user would go stale constantly.
36
38
 
37
- First and only consumer today: the Compass **Assigned Admin** column on the sales-orders table.
39
+ Consumers: Compass **Assigned Admin** (live data, below); `_Model_Client_Certification::_complianceStateFilterOptions()`
40
+ and `_Model_Client_UserCertification::_statusFilterOptions()` (fixed enum list: loop the model's
41
+ state constant, `slug` = `name` = the value; the front end labels it from language data). Use the
42
+ enum shape for any status-like calc column.
38
43
 
39
44
  ## How it works
40
45
 
@@ -56,7 +61,7 @@ an empty dropdown. Same contract as `getStatusFilterOptions()`.
56
61
  throws when a `FIELD_SQL` field has no static of the same name — that is why `_assignedAdmin()`
57
62
  exists. The seam just adds a `FilterOptions` suffix to the same name-derived rule.
58
63
 
59
- It is a **no-op for every existing column**: nothing else declares a `<field>FilterOptions` static.
64
+ It is a **no-op** for any column whose model declares no `<field>FilterOptions` static.
60
65
 
61
66
  ### The Compass example
62
67
 
@@ -161,10 +166,11 @@ generic version at roughly **4–5** such columns, driven by real examples.
161
166
  the same pattern `_assignedManager()` already uses.
162
167
  - **Matching a role by its human-readable `name` is fragile** (flagged by `sql-reviewer`): a client
163
168
  renaming the `Admin` role silently empties the list. A stable role id would be sturdier.
164
- - **⚠ Filtering a calculated column lands in HAVING, not WHERE.**
165
- `api2/Component/Api/V2/V2.php` `splitWhereExpressionsIntoWhereHaving` routes a where on a
166
- calculated field into `HAVING`, so the `_assignedAdmin` correlated subquery is evaluated for every
167
- row surviving the WHERE, **before LIMIT** — and again for the pagination count query. On a large
169
+ - **⚠ Filter with the BARE calc name** (`_complianceState:eq:AT_RISK`) — it is inlined into
170
+ `WHERE`. The table-prefixed form (`Table._x`) goes to `HAVING` and returns 500 / `EO-1`. Mechanism:
171
+ [V2 REST query contract](../../api2/features/v2-rest-query-contract.md). Either way the expression
172
+ (here the `_assignedAdmin` correlated subquery) is evaluated **per row, before LIMIT** — and again
173
+ for the pagination count query. On a large
168
174
  Compass `SalesOrders` table this can be slow, and it will not fail loudly (api2's Apache gateway
169
175
  timeout is 1800s). This is pre-existing behaviour of filtering **any** calculated column, not new
170
176
  here, and it was **not** measured with `EXPLAIN` at production size. The cheaper target would be
@@ -17,6 +17,7 @@
17
17
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | The authoring contract for a Record Script — a `public static` model method that becomes a read-only computed/aggregate `/v2` endpoint (double-nested envelope, |
18
18
  | [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | Where each V2 request lands (client vs core/writer Logs), when it leaves no row at all (PRE-interceptor throw, uncaught fatal), the apiproxy-retry / duplicate-t |
19
19
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | How the V2 engine runs a Record Script for a POST and lets scripted args arrive in the JSON body (to avoid HTTP 414 on large values); open when a scripted API n |
20
+ | [Session currency (audience.currency + JWT id.currency)](features/session-currency.md) | How api2 resolves the user's currency once per session and returns it as `audience.currency` (ISO code or null) and JWT `id.currency`; open when a front end nee |
20
21
  | [TOGa Talos Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The six api2 Record Scripts on `_Model_Team_Sprint` that serve the internal TOGa Talos sprint dashboard from the `Team` schema; open when consuming/registering |
21
22
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | The opt-in `surface=<slug>` GET option that attaches per-record UI action state under `meta.surface` (never `data`); open when wiring the Surface layer to a scr |
22
23
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. |
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-09
9
+ updated: 2026-10-17
10
10
  owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -102,7 +102,7 @@ Placement is load-bearing: the `id.language` mutation happens **after** client-D
102
102
  - **Per-row cost on translatable-join lists (site 5).** `getTranslatedFieldValue` dereferences the source model key via `_Model __get`, forcing a full row load, so ~2 queries per row (source + sidecar) for non-English translatable-join lists — consistent with the feature's per-row-load tradeoff. Candidate future optimization: let the helper accept a known id instead of a live `_Model`.
103
103
  - **Pre-existing (not introduced by site 5): `join=`/`ojoin=` identifiers are unvalidated.** Table/alias/ON-clause identifiers flow from the query string into SQL with no validation/escaping (`V2.php` ~L3416-3463, `parseOptionsJoin` ~L7874-7920). The new PK-select line reuses the same already-tainted `$tableAlias` as the adjacent pre-existing lines, so it adds no new injection class — but the underlying tainted-identifier path is a standing concern flagged for separate follow-up.
104
104
  - Reading a possibly-absent magic field off a generic `_Model` needs `array_key_exists(...)` + `__get` — `isset()`/`?? null` always read false/null. See [_Model magic-field access](../../_underscore/features/model-magic-field-access.md); this was the cause of the uuid not appearing in the fallback warning.
105
- - **Refresh re-resolution is user-token-only.** If you extend the JWT with new audience-shaped claims, remember the refresh path rebuilds context from the *existing* token (uuid/app-uuid/personas) — it does not re-read anything the token doesn't carry.
105
+ - **Refresh re-resolution is user-token-only.** `audience.currency` reuses this cascade + refresh path — see [Session currency](./session-currency.md). If you extend the JWT with new audience-shaped claims, remember the refresh path rebuilds context from the *existing* token (uuid/app-uuid/personas) — it does not re-read anything the token doesn't carry.
106
106
  - Migrations not yet executed at time of writing; needs live verification.
107
107
 
108
108
  ## Related
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Session currency (audience.currency + JWT id.currency)
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-17
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ - api2/Component/Api/V2/Response/Response.php
14
+ - _underscore/Model/Client/Currency.php
15
+ - _underscore/Model/Client/UserGlobalSetting.php
16
+ - _underscore/Model/Client/ItemPrice.php
17
+ - dbchanges2/Core/2026-10-02a - PriceDefaultCurrency.sql
18
+ - dbchanges2/Client/2026-10-02a - AllClients PriceCurrency.sql
19
+ related:
20
+ - ./language-translation-layer.md
21
+ - ../../toga25-blox/features/money-formatting.md
22
+ - ../../../../clients/quad/features/multi-currency-item-pricing.md
23
+ ---
24
+ How api2 resolves the user's currency once per session and returns it as `audience.currency` (ISO code or null) and JWT `id.currency`; open when a front end needs the session currency, a currency setting is ignored, or WZ-5 appears.
25
+
26
+ ## Summary
27
+
28
+ Every authenticated api2 response carries `audience.currency` — a 3-letter ISO code (`USD`) or `null`. The same value is in the JWT `id.currency` claim. It is resolved at sign-in and on `/auth/refresh`, exactly like `audience.language` (one value per session, no per-request cost). Front ends format money with it through blox — see [money formatting](../../toga25-blox/features/money-formatting.md). Uniform across clients.
29
+
30
+ ## How it works
31
+
32
+ **Cascade.** Settings slug `currency` (`Core.Settings` id 14, uuid `daa25a05-c1c7-4dcc-6a7c-c79a3c02b92a`; `Core.DefaultGlobalSettings` value `USD`, seeded by `dbchanges2/Core/2026-10-02a - PriceDefaultCurrency.sql`, present in prod Core — verified 2026-10-17). Walked by the same `resolveSettingValue()` 8-layer cascade as language (DefaultGlobal → … → UserApp, `isOverridable = 0` locks, first persona wins) — see [Language Translation Layer → Language resolution](./language-translation-layer.md#how-it-works).
33
+
34
+ **Two stored shapes — both valid, storage left as-is.**
35
+ - Core default = ISO code (`USD`).
36
+ - Client / persona / user rows = a Client `Currencies.uuid`. Commerce and Quad read that uuid (`_underscore` `UserGlobalSetting` / `ItemPrice` treat the value uuid-first, then code) — do not convert stored rows to codes.
37
+
38
+ **Validation (V2.php).**
39
+ 1. Value looks like a uuid → load `_Model_Client_Currency` by uuid on the user's **own client connection** and return `Currencies.code`. Never a Core↔Client join (separate clusters).
40
+ 2. Otherwise accept only `^[A-Z]{3}$`.
41
+ 3. `''` = not set (next layer wins).
42
+ 4. Unusable value → re-resolve with client-level layers only (default + client, no persona/user); still nothing → `null` + warning **WZ-5** (`Response.php`, `{currency}` placeholder, value cut to 64 chars). Never fails the sign-in or refresh.
43
+
44
+ **Token paths.**
45
+ - Sign-in: resolved, put in `id.currency`, echoed as `audience.currency`.
46
+ - `/auth/refresh` (user tokens only): re-resolved like language, so a change shows on the next refresh.
47
+ - View-as: keeps the delegator's validated currency. A delegator token from before this change (no `currency` key) resolves normally.
48
+ - API-key auth: `null`.
49
+
50
+ ## Gotchas
51
+
52
+ - `Client_Quad` has a `ClientGlobalSettings` currency row with value `''` — counts as not set, falls through to the Core `USD` default.
53
+ - Local `Client_True` / `Client_Elite` `Currencies` tables are empty, so a uuid-valued setting resolves to null + WZ-5 locally. Prod has USD via `dbchanges2/Client/2026-10-02a - AllClients PriceCurrency.sql`. That file has one-time schema steps — do not re-run the whole file locally; insert the row by hand.
54
+ - Changing a user's currency (UI, `isOverridable` fan-out, own-row ACL write guard) is not built yet — later ticket. Mirror the language write guard in [API Payload Interceptors](./api-payload-interceptors.md#own-row-guards-in-a-pre-interceptor) when it is.
55
+
56
+ ## Related
57
+
58
+ - [Language Translation Layer](./language-translation-layer.md) — the shared cascade + refresh mechanics.
59
+ - [Money formatting (toga25-blox)](../../toga25-blox/features/money-formatting.md) — the front-end consumer.
60
+
61
+ ## Change history
62
+ - 2026-10-17 — Added `audience.currency` / `id.currency`, resolved at sign-in + refresh; uuid → code on the client connection; WZ-5 fallback. Proven by a real local sign-in + refresh. TRUE-82878 (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-15
9
+ updated: 2026-10-17
10
10
  owners: [apeterson, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -140,6 +140,7 @@ So the visible failure is a broken SQL query while the actual defect is one miss
140
140
  - **Flag columns are NULLable** — guard flag migrations with `COALESCE(col, 0) = 0`, never `col = 0`, or the UPDATE matches 0 rows and looks like caching.
141
141
  - **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields` changes every view/client that shows the field; there is no per-view type override. `isCopyable`, `isSortable`, `isFilterable`, `isGroupable`, `isVisible` are the per-view knobs (on `TableViewFields`).
142
142
  - **`TableView::meta()` contains a hand-written `SELECT` that names `TableViewFields` columns explicitly.** So a new column on that table is required by **two** mechanisms — the model property *and* that query — and a grep for the property alone under-reports what a missing column breaks.
143
+ - **⚠ Type `SELECT` only on a JOINED FK column.** `meta()`'s `TYPE__SELECT` branch reads `$tableViewJoins[$tableAlias]->record` (`TableView.php` ~L265-272); a main-table column has no join alias, so it breaks at runtime. For an enum column on the main table, expose a calc label field (e.g. `_status`) with a `<field>FilterOptions` static — see [calculated-column filter options](../../_underscore/features/calculated-column-filter-options.md).
143
144
  - **`isGroupable` has no migration** — omit it from new TableView migrations (fails on prod/beta) until the fan-out migration lands. Composite unique keys read as unconstrained in `information_schema.COLUMNS`; use `information_schema.STATISTICS` / `SHOW INDEX`.
144
145
 
145
146
  ## Related
@@ -151,4 +152,5 @@ So the visible failure is a broken SQL query while the actual defect is one miss
151
152
  - [TableView joins](../../_underscore/features/tableview-joins.md)
152
153
 
153
154
  ## Change history
155
+ - 2026-10-17 — `SELECT` type is safe only on a joined FK column; main-table enum → calc label + FilterOptions. (jcardinal)
154
156
  - 2026-09-23 — Recorded that `Core.RecordFields.type` drives display/filter only and never sorting; that `NUMBER` on a text column blanks non-numeric rows (Compass `kit 205-207` bundles) while `BOOLEAN` fixes genuinely blank flag columns; and that the flag columns are NULLable so a `= 0` migration guard matches nothing. (apeterson)
@@ -56,6 +56,10 @@ controller.
56
56
  (`V2.php` ~L3673–3682). **Prefer bare for ascending:** the query string is parsed *without*
57
57
  urldecoding (see Encoding rules), so a client that percent-encodes the `+` as `%2B` sends a
58
58
  field literally named `%2Bfield` and the sort is silently dropped. Bare has no such failure mode.
59
+ - **Multi-sort = comma list** (`sort=-dateIssued,uuid`; `parseOptionsSort` ~L10888 explodes on `,`;
60
+ a blox `sort: [...]` array is sent comma-joined). **Offset paging (`page`) is only stable with a
61
+ unique last key** — add `uuid` as the tie-breaker, or rows with equal sort values shuffle between
62
+ pages.
59
63
  - **A LIST select is `SELECT SQL_CALC_FOUND_ROWS DISTINCT <table>.<primaryKey>`** (`V2.php:3884`;
60
64
  the aggregate-fields path is the one exception). Two consequences: (a) join fan-out collapses to
61
65
  one row per record, so **duplicate child rows are invisible in the response** — a duplicate join
@@ -382,9 +386,10 @@ Mechanism, read from `api2/Component/Api/V2/V2.php`:
382
386
  1. `buildWhereExpressionsFromOptions` (~L6953) **deliberately does not prepend the default table**
383
387
  to a field starting with `_` — its own comment says *"add the default table if not specified and
384
388
  is not a calculated field."*
385
- 2. `buildWhereClauseFromWhereExpressions` (~L7069) sees the leading `_` and **inlines the field's own
386
- SQL expression** straight into the `WHERE` clause. That is the working path.
387
- 3. `splitWhereExpressionsIntoWhereHaving` (~L8622) routes to `HAVING` **only when the field contains
389
+ 2. `validateWhereFieldReference` (~L8704-8741) accepts a bare calc name on the primary table.
390
+ 3. `buildWhereClauseFromWhereExpressions` (~L9225-9234) sees the leading `_` and **inlines the field's
391
+ own SQL expression** straight into the `WHERE` clause. That is the working path.
392
+ 4. `splitWhereExpressionsIntoWhereHaving` (~L11286) routes to `HAVING` **only when the field contains
388
393
  `._`** — i.e. only the table-**prefixed** form. That form targets a joined-table select alias
389
394
  (`PurchaseOrderItems__qtyAvailable`, built ~L3984) which **does not exist for the main table**,
390
395
  and ~L4116 also appends the `HAVING` to the `$sqlProhibited` COUNT query, which has no such alias
@@ -411,9 +416,11 @@ production — `toga2-supply/src/api/toga.ts` L906-919 (`_status` OR group) and
411
416
  **Zero** occurrences of the table-prefixed form exist in any repo — nobody has ever used it
412
417
  successfully.
413
418
 
414
- > ⚠ **Scope this to `where` only.** It says nothing about `group` or `sort` on a calculated field;
415
- > `group=_status` is separately known to fail. And `where` still never prunes a nested child array
416
- > (see below).
419
+ > **`sort` on a calculated field also works** (bare name, calc branch of `ORDER BY` above).
420
+ > **`group=_status` fails.** `where` still never prunes a nested child array (see below).
421
+ > A TableView column over a calc field can be `isFilterable` — a code comment saying "calc fields
422
+ > are not filterable" records a choice, not an engine limit. Dropdown options:
423
+ > [calculated-column filter options](../../_underscore/features/calculated-column-filter-options.md).
417
424
 
418
425
  #### ⚠ `in` instead of an OR-chain of `contains` shortens the SQL - it does NOT make the filter fast
419
426
 
@@ -618,6 +625,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
618
625
  [V2 request logging](request-logging.md).
619
626
 
620
627
  ## Change history
628
+ - 2026-10-17 — Multi-sort comma list + `uuid` tie-breaker for stable paging; sort on calc works; refreshed where-path line refs. (jcardinal)
621
629
  - 2026-10-17 — POST create = 201, PUT/DELETE = 200 (TRUE-82889). (jcardinal)
622
630
  - 2026-10-15 — Security: `where` on an unreadable field still filters under default `identifier_acl_mode=log` (hash guessing); dotted FK path in `where` = `EV-8`, use a join alias (TRUE-82836). Folded older history. (jcardinal)
623
631
  - 2026-09-24 — `depth`/`calcDepth` ignored inside the base64 `_` blob; field three levels under an inherent child dropped (TRUE-82138). (apeterson)
@@ -4,6 +4,7 @@
4
4
  |-----|---------|
5
5
  | [TOGa Blox 2.5 — Architecture](architecture.md) | Shared React+TS SOURCE library for the new platform apps (new platform apps first; replaces toga-blox for all apps over time); read the ADR for full detail, not |
6
6
  | [Contact primary email/phone save (updateContactChannels) — table calls only](features/contact-channels-save.md) | How a contact edit form saves the contact's primary email and phone without nested writes; use it (do not rebuild it) in any app that edits a contact's email/ph |
7
+ | [Money formatting (useSessionCurrency / formatMoney / useFormatMoney)](features/money-formatting.md) | The one money formatter for every platform app (session currency + session language via Intl); open before showing any money amount or adding a money column. |
7
8
  | [Old blox (toga-blox) straight port inside toga25-blox](features/old-blox-port.md) | Old `@agilant/toga-blox` code copied unchanged into toga25-blox (blox-v1 layer); open before touching any old component, its checks/config, or merging Storybook |
8
9
  | [GitHub Actions → AWS role via OIDC (trust policy + sub claim)](workflows/github-actions-aws-oidc-role.md) | How a GitHub Actions job assumes an AWS IAM role with OIDC, and the `sub` claim trap; open before creating or debugging any GitHub-to-AWS role ("Not authorized |
9
10
  | [Moving an old app from @agilant/toga-blox onto toga25-blox](workflows/moving-an-old-app-onto-toga25-blox.md) | Planned steps to switch an old app off the `@agilant/toga-blox` npm package onto toga25-blox source with zero import edits; open before moving any old app. |
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: Money formatting (useSessionCurrency / formatMoney / useFormatMoney)
3
+ framework: "2.0"
4
+ repo: toga25-blox
5
+ project: TOGa Blox 2.5
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-17
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - toga25-blox/src/core/format/formatMoney.ts
13
+ - toga25-blox/src/core/auth/useSessionCurrency.ts
14
+ - toga25-blox/src/core/http/currencyStore.ts
15
+ - toga25-blox/src/data-table/record-table/cellRegistry.tsx
16
+ - toga25-blox/src/data-table/record-table/ColumnHeaderControls.tsx
17
+ related:
18
+ - ../../api2/features/session-currency.md
19
+ - ../architecture.md
20
+ ---
21
+ The one money formatter for every platform app (session currency + session language via Intl); open before showing any money amount or adding a money column.
22
+
23
+ ## Summary
24
+
25
+ `formatMoney(value, {currency, locale})` and the hook `useFormatMoney()` are THE money formatter for every platform app. The currency comes from `useSessionCurrency()` (api2 `audience.currency` — see [session currency](../../api2/features/session-currency.md)); the locale is the session language. Never hard-code a currency or a `$` in front-end code.
26
+
27
+ ## How it works
28
+
29
+ - `currencyStore.ts` keeps `audience.currency` from each api2 response (wired in `apiClient.ts`, `credentials.ts`, `auth/session.ts`). `useSessionCurrency()` returns the ISO code or `null`.
30
+ - `formatMoney` uses `Intl.NumberFormat` with the session language as locale and the ISO code as currency. It does not use `Currencies.symbol` / `position` — Intl places the symbol per language.
31
+ - `null` or invalid currency → the number with no symbol (never a guessed `$`).
32
+ - Record-table default cells format money through `cellContext.formatMoney` — a money column needs no custom cell.
33
+ - **Right-aligned columns:** set `align: "right"` on a blox column (header + cells; the label stays first in tab order). Do not write app CSS that reaches into blox table internals to align a column.
34
+
35
+ **Which currency for an amount (decided 2026-10-17, jcardinal):**
36
+ - A stored amount with no currency column of its own is in its **owning client's** currency.
37
+ - Multi-client listings use the session currency for now — all clients are USD.
38
+ - Formatting by owning client must be built **before any client goes non-USD** (tracked follow-up).
39
+
40
+ ## Gotchas
41
+
42
+ - Quad stores many currencies per item — its listings render per-currency text, not one formatted number. See [Quad multi-currency item pricing](../../../../clients/quad/features/multi-currency-item-pricing.md).
43
+
44
+ ## Change history
45
+ - 2026-10-17 — Added session currency + shared money formatter; record-table money cells and `align: "right"` columns. TRUE-82878 (jcardinal)
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-05
8
+ updated: 2026-10-17
9
9
  owners: [jcardinal, apeterson, tcox]
10
10
  files: []
11
11
  related:
@@ -755,6 +755,17 @@ Copy from there rather than reinventing.
755
755
 
756
756
  ---
757
757
 
758
+ ## 24. Money display — **MUST**
759
+
760
+ - Format every money amount with toga25-blox `useFormatMoney()` / `formatMoney()` (session
761
+ currency from api2 `audience.currency`, session language as locale). Never hard-code a
762
+ currency code or `$`, and never build the symbol from `Currencies.symbol`/`position`.
763
+ - Money columns: use the record-table default money cell and blox column `align: "right"` —
764
+ no app CSS reaching into blox table internals.
765
+ - Detail + the "owning client's currency" decision: [Money formatting](../apps/toga25-blox/features/money-formatting.md).
766
+
767
+ ---
768
+
758
769
  # Part 3 — Open items
759
770
 
760
771
  *(Described and flagged. NOT prescribed as settled.)*
@@ -23,7 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
23
23
 
24
24
  - **_underscore** (_Underscore) _(framework core)_ — 104 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
25
  - **worker2** (Worker) — 90 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
- - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
26
+ - **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
28
28
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
29
29
  - **saml** (SAML SSO Gateway) — 9 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
@@ -34,7 +34,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
34
34
  - **toga25-supply** (TOGa 2.5 Supply) — 26 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
36
36
  - **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
37
- - **toga25-blox** (TOGa Blox 2.5) — 5 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
37
+ - **toga25-blox** (TOGa Blox 2.5) — 6 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
38
38
  - **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
39
39
 
40
40
  ## standalone framework
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1069",
3
+ "version": "1.0.1071",
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",