toga-ai 1.0.746 → 1.0.748
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/note-record-authorship.md +101 -0
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +22 -0
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +154 -33
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga-blox/features/advanced-select.md +14 -1
- package/knowledge/2.0/apps/toga-blox/features/table-record-modal.md +34 -1
- package/knowledge/2.0/apps/toga-blox/features/talos-assistant.md +50 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/talos-integration.md +52 -10
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +275 -27
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/talos-assistant-enablement.md +110 -26
- package/knowledge/clients/nychh/features/transfer-order-inventory-quantities.md +40 -1
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
| [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php, _underscore/Model/Rate/Subscription.php |
|
|
34
34
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, _underscore/Trait/Netsuite/SalesOrder.php, worker2/Worker/Netsuite/SalesOrder.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Worker/Netsuite/Opportunity.php |
|
|
35
35
|
| [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models** (every client on the dbchanges2 `netsuite` module). | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Model.php, dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql |
|
|
36
|
+
| [Note records have NO author — every `*Notes` model's `contactId` is unwritten](features/note-record-authorship.md) | **"Who wrote this note?" is currently unanswerable from TOGa 2.0 data.** Every note model in the framework declares a `contactId` foreign key to `Contacts` — th | _underscore/Model/Client/SalesOrderNote.php, _underscore/Model/Client/PurchaseOrderNote.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TicketNote.php, _underscore/Model/Client/UnitNote.php, _underscore/Model/Client/ServiceRequestNote.php, _underscore/Model/Client/PartRequestNote.php |
|
|
36
37
|
| [Outbound third-party API rate budget (_Component_Api_RateBudget) — designed + staged, NOT merged](features/outbound-api-rate-budget.md) | A **shared outbound rate budget for every third-party API** we call from 2.0 (ClickUp, NetSuite, FedEx, UPS, …): one small component plus one `Core` table, keye | _underscore/Component/Api/RateBudget/RateBudget.php, _underscore/Component/Api/Clickup/Clickup.php, _underscore/Route.php, _underscore/Query.php, dbchanges2/Core/2026-09-01a - Add ApiRateBudget table.sql, worker2/Worker/Clickup/Api.php |
|
|
37
38
|
| [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php, _underscore/Model/Client/TableView.php, toga2-supply/src/components/ui/Tables/PrimaryTable/PrimaryTable.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx |
|
|
38
39
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Note records have NO author — every `*Notes` model's `contactId` is unwritten"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-02
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/SalesOrderNote.php
|
|
13
|
+
- _underscore/Model/Client/PurchaseOrderNote.php
|
|
14
|
+
- _underscore/Model/Client/TransferOrderNote.php
|
|
15
|
+
- _underscore/Model/Client/TicketNote.php
|
|
16
|
+
- _underscore/Model/Client/UnitNote.php
|
|
17
|
+
- _underscore/Model/Client/ServiceRequestNote.php
|
|
18
|
+
- _underscore/Model/Client/PartRequestNote.php
|
|
19
|
+
related:
|
|
20
|
+
- ../../toga-blox/features/add-notes-template.md
|
|
21
|
+
- ../../toga25-supply/features/transfer-orders-page.md
|
|
22
|
+
- ../features/sales-order-denial-reason.md
|
|
23
|
+
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
**"Who wrote this note?" is currently unanswerable from TOGa 2.0 data.**
|
|
29
|
+
|
|
30
|
+
Every note model in the framework declares a `contactId` foreign key to `Contacts` — the field that
|
|
31
|
+
*looks* like note authorship — and **no code anywhere writes it.** Note rows are created with the
|
|
32
|
+
body, a type and a timestamp, and no author. Read that as a schema affordance that was never wired
|
|
33
|
+
up, not as a column you can query.
|
|
34
|
+
|
|
35
|
+
Found 2026-09-01 while building `TransferOrderNote` / `TransferOrderNoteType` as faithful mirrors of
|
|
36
|
+
the purchase-order pair; the gap turned out to be platform-wide, not new.
|
|
37
|
+
|
|
38
|
+
## The seven models, all the same shape
|
|
39
|
+
|
|
40
|
+
Verified against `_underscore` (`grep contactId Model/Client/*Note.php`) — **all seven** declare it:
|
|
41
|
+
|
|
42
|
+
```php
|
|
43
|
+
public $contactId = [self::FIELD_FOREIGNKEY, self::FIELDOPT_FOREIGNKEY_MODEL => '\_Model_Client_Contact'];
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Model | Table |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `_Model_Client_SalesOrderNote` | `SalesOrderNotes` |
|
|
49
|
+
| `_Model_Client_PurchaseOrderNote` | `PurchaseOrderNotes` |
|
|
50
|
+
| `_Model_Client_TransferOrderNote` | `TransferOrderNotes` |
|
|
51
|
+
| `_Model_Client_TicketNote` | `TicketNotes` |
|
|
52
|
+
| `_Model_Client_UnitNote` | `UnitNotes` |
|
|
53
|
+
| `_Model_Client_ServiceRequestNote` | `ServiceRequestNotes` |
|
|
54
|
+
| `_Model_Client_PartRequestNote` | `PartRequestNotes` |
|
|
55
|
+
|
|
56
|
+
The rest of the shape is uniform too: `<parent>Id`, `<parent>NoteTypeId`, `dtStamp`
|
|
57
|
+
(`FIELD_DATETIME_CREATED`), `note`. So `dtStamp` tells you **when**, the type tells you **what kind**
|
|
58
|
+
— and nothing tells you **who**.
|
|
59
|
+
|
|
60
|
+
## The consequence, and the workaround that exists in production
|
|
61
|
+
|
|
62
|
+
Because the column is empty, any "submitted by" display has to recover the author from the note
|
|
63
|
+
**body text**. Compass does exactly that: it **regex-scrapes email addresses out of the note body**.
|
|
64
|
+
That works only because their notes happen to contain an address; it is not a pattern to copy, and it
|
|
65
|
+
fails silently on any note that does not.
|
|
66
|
+
|
|
67
|
+
So before building anything that shows or filters by note author:
|
|
68
|
+
|
|
69
|
+
- **Do not assume `contactId` is populated** — check the actual rows for the tenant first. It will be
|
|
70
|
+
NULL.
|
|
71
|
+
- **Do not add a per-tenant body-text scrape.** That is the existing workaround, and it is the reason
|
|
72
|
+
this gap has stayed invisible.
|
|
73
|
+
- **The real fix is to write `contactId` on create**, from the authenticated identity, in whichever
|
|
74
|
+
layer creates the note. That is unclaimed platform work — it also needs a backfill decision for the
|
|
75
|
+
existing rows, which cannot be recovered except by scraping.
|
|
76
|
+
|
|
77
|
+
## Why a transfer's Notes box does not use these tables at all
|
|
78
|
+
|
|
79
|
+
The toga25-supply Create Transfer Order modal deliberately writes **`TransferOrders.description`**
|
|
80
|
+
(one `varchar(255)` free-text field) instead of note rows: a transfer carries one description, not a
|
|
81
|
+
thread of typed notes from different authors. The authorship gap documented here is part of why —
|
|
82
|
+
a note thread whose entries have no author is a weaker record than a single description field. The
|
|
83
|
+
`TransferOrderNotes` tables and their Core metadata were built anyway and are currently **unused**;
|
|
84
|
+
see [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
85
|
+
|
|
86
|
+
## Gotchas
|
|
87
|
+
|
|
88
|
+
- **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models,
|
|
89
|
+
zero writers.
|
|
90
|
+
- **A notes list UI has no author to show.** Design for it, or the field silently renders blank for
|
|
91
|
+
every row.
|
|
92
|
+
- **`dtStamp` is `FIELD_DATETIME_CREATED`**, so it is stamped by the ORM on insert and is the only
|
|
93
|
+
provenance a note actually carries.
|
|
94
|
+
|
|
95
|
+
## Change history
|
|
96
|
+
- 2026-09-02 — First capture. Recorded that **all seven** `*Notes` models (`SalesOrderNote`,
|
|
97
|
+
`PurchaseOrderNote`, `TransferOrderNote`, `TicketNote`, `UnitNote`, `ServiceRequestNote`,
|
|
98
|
+
`PartRequestNote`) declare a `contactId` FK to `Contacts` that **no code writes**, so note
|
|
99
|
+
authorship is unrecoverable from the data; Compass works around it by regex-scraping email
|
|
100
|
+
addresses out of the note body. Surfaced while building the (now unused) `TransferOrderNote` /
|
|
101
|
+
`TransferOrderNoteType` pair for NYCHH transfer orders. (apeterson)
|
|
@@ -143,6 +143,21 @@ GET /v2/transfer-orders/{uuid}/transfer-order-items
|
|
|
143
143
|
- **The JSON key of an inherent child is the camelCase of the child ROUTE**, not of the table or the
|
|
144
144
|
model (`V2.php` ~L6310, via `_String::CASE_KEBAB` → `CASE_CAMEL`). `transfer-order-items` →
|
|
145
145
|
`transferOrderItems`.
|
|
146
|
+
- **🚨 The same route-derived key governs the WRITE path — so the ROUTE is the payload contract.**
|
|
147
|
+
`V2.php:3248` builds `$lookupInherentChildByFieldName` by converting each inherent child's `route`
|
|
148
|
+
kebab→camel, and the **CREATE** branch reuses that *same* lookup at `V2.php:5241`
|
|
149
|
+
(`$childrenToCreate[$key] = $val`). So a nested collection in a `POST` body must be keyed by the
|
|
150
|
+
child **route** in camelCase — not the table, not the model, not the FE's preferred name. Two
|
|
151
|
+
consequences: (a) **renaming a `Core.Records.route` silently renames the payload key of every
|
|
152
|
+
writer**, and nothing in the database enforces or warns about it; (b) a key that matches nothing
|
|
153
|
+
falls through to `$unknownFields` (`V2.php:5251`). Worked example: the toga25-supply Create
|
|
154
|
+
Transfer Order modal posts header + lines in one request as `transferOrderItems` — see
|
|
155
|
+
[Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
156
|
+
> ⚠ **Do not assume the `$unknownFields` error protects you.** It is a *loud* rejection on this
|
|
157
|
+
> branch, but a wrong-shaped nested collection has been measured returning **201 with the data
|
|
158
|
+
> silently dropped** on other paths — see
|
|
159
|
+
> [nested-relationship writes](nested-relationship-writes.md). Match the route exactly and
|
|
160
|
+
> **assert child row counts**; never take a 2xx as proof the children landed.
|
|
146
161
|
- **Adding an `InherentRecordChildren` row is a breaking-shape change, not a feature flag.** It
|
|
147
162
|
alters the default response for **every** existing caller of that route — including 1.0 importers
|
|
148
163
|
and NetSuite sync — so the blast radius has to be surveyed first. The transfer-order build
|
|
@@ -364,6 +379,13 @@ ACL gate for calculated fields lives at **:7216** in `getFullModelData()` — wh
|
|
|
364
379
|
[V2 request logging](request-logging.md).
|
|
365
380
|
|
|
366
381
|
## Change history
|
|
382
|
+
- 2026-09-02 — Recorded that the inherent-child key rule **also governs the WRITE path**, so the
|
|
383
|
+
child **route** is the payload contract for a nested collection in a `POST` body: `V2.php:3248`
|
|
384
|
+
builds `$lookupInherentChildByFieldName` (route kebab→camel) and the CREATE branch reuses that
|
|
385
|
+
same lookup at `V2.php:5241` (`$childrenToCreate`), with unmatched keys collected into
|
|
386
|
+
`$unknownFields` at `:5251`. So renaming a `Core.Records.route` silently renames every writer's
|
|
387
|
+
payload key, with nothing in the database to warn about it. Verified against the engine while
|
|
388
|
+
making the Create Transfer Order modal post header + lines in a single request. (apeterson)
|
|
367
389
|
- 2026-09-02 — Recorded that a **bridge** collection needs its own `InherentRecordChildren` row and
|
|
368
390
|
that the **header** bridge is a different record from the item bridge: a transfer order's PO number
|
|
369
391
|
reads through `312 → 325 purchase-orders-transfer-orders`, **not** `329
|
|
@@ -1096,19 +1096,49 @@ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that kee
|
|
|
1096
1096
|
`Core/2026-09-02a - TalosAssistantSurfaceSeed.sql` +
|
|
1097
1097
|
`Client_Nychh/2026-09-02a - TalosAssistantEnable.sql` put the Talos AI assistant behind the
|
|
1098
1098
|
surface layer. The surface is `talos-assistant`, **type `DRAWER`**, **not record-bound**, with
|
|
1099
|
-
`appId` **inherited from the `navigation` sibling by slug** (never a literal).
|
|
1100
|
-
**
|
|
1099
|
+
`appId` **inherited from the `navigation` sibling by slug** (never a literal). **TWO elements**,
|
|
1100
|
+
**both seeded `isVisible = 0`** — grant-only:
|
|
1101
1101
|
|
|
1102
|
-
|
|
|
1103
|
-
|
|
1104
|
-
| `config.role = 'featureEnabled'`, `config.key = 'feature'` |
|
|
1105
|
-
| `config.role = 'suggestion'`
|
|
1102
|
+
| Element | Purpose |
|
|
1103
|
+
|---|---|
|
|
1104
|
+
| `config.role = 'featureEnabled'`, `config.key = 'feature'` | **MASTER SWITCH.** Its `isVisible` turns the launcher, the panel and the whole AI stream on or off. Stays a **plain boolean grant** — see why below. |
|
|
1105
|
+
| `config.role = 'suggestion'`, `config.key = 'suggestions'` | **THE WHOLE CHIP ROW, in ONE element.** `config.items` is an **ordered JSON array** of `{label, prompt, icon}` (`icon` = a literal FontAwesome name). Its own `isVisible` switches the entire row. |
|
|
1106
1106
|
|
|
1107
1107
|
It replaced `DEFAULT_SUGGESTIONS` — four hardcoded **TOGa Supply** questions that lived inside
|
|
1108
1108
|
`toga-blox`, a library five apps consume, so any host mounting the panel inherited Supply's
|
|
1109
1109
|
prompts. FE side:
|
|
1110
1110
|
[Talos integration](../../toga25-supply/features/talos-integration.md).
|
|
1111
1111
|
|
|
1112
|
+
### ⚠ SUPERSEDED — the chips were briefly ONE ELEMENT PER CHIP. They are not.
|
|
1113
|
+
|
|
1114
|
+
The first cut of this seed wrote **five** elements: the marker plus **four `config.role='suggestion'`
|
|
1115
|
+
elements, one per chip** (`labelMessageId` for the label, `config.prompt` for the question,
|
|
1116
|
+
`iconToken`/`iconWeightToken` for the icon, `sortOrder` for the order). **That shape is gone** —
|
|
1117
|
+
replaced before anything ran past local. If you find a doc, a branch or a local DB in that shape, it
|
|
1118
|
+
is the old design. There is **no `config.prompt`**, no per-chip `labelMessageId`, and **no per-chip
|
|
1119
|
+
`sortOrder`** any more; order is array order.
|
|
1120
|
+
|
|
1121
|
+
**Why one element wins.** Per-chip elements let a tenant only switch **on** questions Core had
|
|
1122
|
+
already seeded, so changing a tenant's *wording* needed a new **Core** migration for every phrasing.
|
|
1123
|
+
These are the tenant's own business questions — Core must not gatekeep the wording. With one
|
|
1124
|
+
element a tenant **authors its own set** in a single `CONFIG` override, and adding or reordering a
|
|
1125
|
+
chip is **one row instead of a Core deploy**.
|
|
1126
|
+
|
|
1127
|
+
**Two costs, accepted deliberately — do not rediscover them:**
|
|
1128
|
+
|
|
1129
|
+
1. **Chip labels are NOT translatable.** They are raw strings in `config`, not `Messages` keys, so
|
|
1130
|
+
`MessageTranslations` cannot reach them. (Partly moot: API-auth JWTs carry no language, so client
|
|
1131
|
+
translations never apply on those sessions anyway.)
|
|
1132
|
+
2. **Only whole SETS are scopable, not individual chips — and a multi-role user resolves
|
|
1133
|
+
arbitrarily.** Two role-scoped `CONFIG` rows resolve by
|
|
1134
|
+
`ORDER BY (roleId IS NULL) DESC, (personaId IS NULL) DESC, id ASC`, so for a user holding **both**
|
|
1135
|
+
roles the **higher id simply wins**. There is no merge and no defined precedence between two peer
|
|
1136
|
+
roles.
|
|
1137
|
+
|
|
1138
|
+
That second cost is exactly **why the `featureEnabled` marker was NOT folded into the config**: a
|
|
1139
|
+
plain boolean grant keeps the grant-only model (and its clean shadowing rules) for the thing that
|
|
1140
|
+
actually gates access.
|
|
1141
|
+
|
|
1112
1142
|
### 🚨 A feature switch is a MARKER ELEMENT — never `Surfaces.isVisible`
|
|
1113
1143
|
|
|
1114
1144
|
The override cascade is keyed by **`surfaceElementId`** and is **never applied to a `Surfaces`
|
|
@@ -1116,7 +1146,7 @@ row's own `isVisible`** (`_loadOverrides`, `Model/Core/Surface.php` ~L733–748)
|
|
|
1116
1146
|
surface row silently does nothing.** So a whole-feature switch has to be a hidden **marker
|
|
1117
1147
|
element** on the surface, exactly like the `sectionVisibility` and `approvalsEnabled` markers.
|
|
1118
1148
|
|
|
1119
|
-
### The
|
|
1149
|
+
### The prompts live in `config.items[]`, NOT an `Actions` row
|
|
1120
1150
|
|
|
1121
1151
|
`RUN_TALOS` **is** a member of the `Actions.type` ENUM, but **nothing in `dbchanges2` has ever
|
|
1122
1152
|
seeded an `Actions` row** and there is **no FE action-registry entry** for it. `config` is emitted
|
|
@@ -1125,6 +1155,44 @@ wiring**. Same trade `Core/2026-08-12a` made choosing `config.path` over `Action
|
|
|
1125
1155
|
the same discriminator as the BUTTON_BAR rule above: **seed an `Action` only when the behaviour
|
|
1126
1156
|
lives in the registry.**
|
|
1127
1157
|
|
|
1158
|
+
### 🚨 A `CONFIG` override REPLACES `config` wholesale — this is the load-bearing rule of the whole design
|
|
1159
|
+
|
|
1160
|
+
Verified in `_underscore` **`Model/Core/Surface.php:839`**: the decoded JSON is **assigned over**
|
|
1161
|
+
`$config` —
|
|
1162
|
+
|
|
1163
|
+
```php
|
|
1164
|
+
$config = (is_array($value) || is_object($value)) ? $value : $config;
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
— **there is no `array_merge`.** Consequences, in order of how much time they cost:
|
|
1168
|
+
|
|
1169
|
+
- **A tenant override MUST restate `"role":"suggestion"` alongside its `items`.** Send only `items`
|
|
1170
|
+
and `config.role` is **wiped**, the frontend stops recognising the element, and **the chips
|
|
1171
|
+
silently disappear** — no error in the API, the console, or the bundle. Nothing anywhere says
|
|
1172
|
+
why.
|
|
1173
|
+
- **A top-level JSON array passes that check**, which is precisely why sending a bare
|
|
1174
|
+
`[{...},{...}]` is the tempting wrong move. It is accepted, stored, resolved — and roleless.
|
|
1175
|
+
- Verify a chip override by asserting the **role survived**, not just that the row exists:
|
|
1176
|
+
`JSON_UNQUOTE(JSON_EXTRACT(o.c_longValue,'$.role'))` must be non-NULL.
|
|
1177
|
+
|
|
1178
|
+
**Put the JSON in `c_longValue`, never `value`.** `value` is `varchar(255)` and any real chip set
|
|
1179
|
+
overflows it. The resolver falls back to `c_longValue` when `value` is `NULL` or `''`
|
|
1180
|
+
(`Surface.php:907`). `_castOverrideValue` `json_decode`s `CONFIG` / `VISIBILITY_RULE` /
|
|
1181
|
+
`ENABLED_RULE`, and on bad JSON it **`error_log`s and returns null** — so a typo degrades to "the
|
|
1182
|
+
override was never there", again silently.
|
|
1183
|
+
|
|
1184
|
+
### Teardown belongs IN the file, and the ORDER is not optional
|
|
1185
|
+
|
|
1186
|
+
Both files carry a commented `TEARDOWN` block, because this shape changed **after** the files had
|
|
1187
|
+
already been run on local. Correct order:
|
|
1188
|
+
|
|
1189
|
+
1. `DELETE` the `Client_Nychh.SurfaceOverrides` rows **first**.
|
|
1190
|
+
2. Then the Core `SurfaceElements` rows.
|
|
1191
|
+
3. Then the `Surfaces` row.
|
|
1192
|
+
|
|
1193
|
+
Reverse it and the grants point at element ids that no longer exist. The `talos.feature.name`
|
|
1194
|
+
`Message` is **deliberately left behind** — harmless, and re-used on a re-run.
|
|
1195
|
+
|
|
1128
1196
|
### ⚠ Second authoring pattern: AUTO_INCREMENT + `NOT EXISTS`, for a PURELY ADDITIVE seed
|
|
1129
1197
|
|
|
1130
1198
|
The reserved-literal-id house style above (2026-08-24) remains the default. **These two files
|
|
@@ -1197,35 +1265,88 @@ user — including devteam — gets Talos. That matches how NYCHH already grants
|
|
|
1197
1265
|
After a seed that consumes ids, post **"next `<Table>.id` = N"** for teammates. The number to
|
|
1198
1266
|
publish is the **highest `MAX(id)+1` across EVERY environment**, not just local.
|
|
1199
1267
|
|
|
1268
|
+
#### `Core.Records` / `Core.RecordFields` — the two HAND-MAINTAINED id tables: ASK, don't derive
|
|
1269
|
+
|
|
1270
|
+
These two are **not** like `Surfaces`/`SurfaceElements`/`Messages`. Platform-wide they are the
|
|
1271
|
+
**only** tables whose ids are **team-assigned by hand**, which is precisely what makes them safe to
|
|
1272
|
+
inline as literals in a `Client_*` file across the Core/Client cluster split. Three rules, in order:
|
|
1273
|
+
|
|
1274
|
+
1. **Ask the developer for the next free id.** The team maintains the running number and posts it
|
|
1275
|
+
after every id-consuming migration. That is the primary source — not a query, not the repo.
|
|
1276
|
+
2. **Never `AUTO_INCREMENT` a `Records` / `RecordFields` insert.** A generated id differs per
|
|
1277
|
+
environment and instantly breaks every literal that points at it.
|
|
1278
|
+
3. **Never infer the next id from `dbchanges2`** — **the repo lags production.** Rows get
|
|
1279
|
+
hand-applied ahead of (or instead of) a migration file, so the highest id in the repo is routinely
|
|
1280
|
+
*below* the real ceiling. Verify against **production** before running.
|
|
1281
|
+
|
|
1282
|
+
Consumed on **2026-09-01** by the transfer-order contact/notes work — verified against production
|
|
1283
|
+
first (`MAX(Records.id)` = **358**; `RecordFields` **2561–2563** free), then used: Records
|
|
1284
|
+
**357**/**358** and RecordFields **2550–2560** + **2561–2563**. **Handoff published: next
|
|
1285
|
+
`Records.id` = 359, next `RecordFields.id` = 2564.** ⚠ Re-verify against production immediately
|
|
1286
|
+
before your own run — that handoff is a snapshot, and the migrations that consumed it were still
|
|
1287
|
+
uncommitted at the time. See
|
|
1288
|
+
[Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
1289
|
+
|
|
1200
1290
|
## Change history
|
|
1291
|
+
- 2026-09-02 — Wrote down the **`Core.Records` / `Core.RecordFields` id protocol**, the counterpart to
|
|
1292
|
+
this doc's reserved-block discipline: they are the only two hand-maintained-id tables platform-wide,
|
|
1293
|
+
so **ask the developer for the next free id**, never `AUTO_INCREMENT` them, and **never infer the
|
|
1294
|
+
next id from `dbchanges2` — the repo lags production** (rows are hand-applied ahead of files).
|
|
1295
|
+
Consumed 2026-09-01 by the transfer-order contact/notes work after verifying against production
|
|
1296
|
+
(`MAX(Records.id)` = 358): Records 357/358 and RecordFields 2550–2563. Handoff published: next
|
|
1297
|
+
`Records.id` = 359, next `RecordFields.id` = 2564. (apeterson)
|
|
1298
|
+
|
|
1201
1299
|
- 2026-09-02 — Added the **Talos feature-gate seed pair** (`Core/2026-09-02a -
|
|
1202
1300
|
TalosAssistantSurfaceSeed.sql`: surface `talos-assistant`, type **DRAWER**, not record-bound,
|
|
1203
|
-
`appId` inherited from the `navigation` sibling by slug
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
`
|
|
1227
|
-
|
|
1228
|
-
|
|
1301
|
+
`appId` inherited from the `navigation` sibling by slug; `Client_Nychh/2026-09-02a -
|
|
1302
|
+
TalosAssistantEnable.sql`). **REBUILT mid-session and this entry records the FINAL shape:** Core
|
|
1303
|
+
seeds **TWO** elements, both `isVisible = 0` — the `config.role='featureEnabled'` marker, and
|
|
1304
|
+
**ONE** `config.role='suggestion'` / `config.key='suggestions'` element whose `config.items` is an
|
|
1305
|
+
ordered JSON array of `{label, prompt, icon}`. The first cut wrote **five** elements (one per
|
|
1306
|
+
chip, prompt in `config.prompt`, order in `sortOrder`); **that shape is superseded** — per-chip
|
|
1307
|
+
elements let a tenant only switch on wording **Core** had already seeded, so every rephrasing
|
|
1308
|
+
needed a Core migration for questions that are the tenant's own business. NYCHH now writes **three**
|
|
1309
|
+
rows: two `IS_VISIBLE='1'` grants (`feature`, `suggestions`) plus **one `CONFIG` override
|
|
1310
|
+
authoring its own three questions**. Two costs accepted: chip labels are raw config strings, **not
|
|
1311
|
+
`Messages` keys**, so `MessageTranslations` can never translate them (partly moot — API-auth JWTs
|
|
1312
|
+
carry no language); and only whole **sets** are scopable, so a **multi-role user resolves
|
|
1313
|
+
arbitrarily** (`ORDER BY (roleId IS NULL) DESC, (personaId IS NULL) DESC, id ASC` — higher id
|
|
1314
|
+
wins, no merge). That is exactly why the `featureEnabled` marker was **kept a plain boolean grant**
|
|
1315
|
+
and not folded into the config. **DECIDED/DISCOVERED, reusable:** (1) a whole-feature switch must
|
|
1316
|
+
be a hidden **marker element**, because the override cascade is keyed by `surfaceElementId` and is
|
|
1317
|
+
**never** applied to a `Surfaces` row's own `isVisible` (`_loadOverrides`, `Surface.php`
|
|
1318
|
+
~L733–748) — overriding the surface row silently does nothing; (2) 🚨 a **`CONFIG` override
|
|
1319
|
+
REPLACES `config` wholesale, it does not merge** — verified at `Surface.php:839`
|
|
1320
|
+
(`$config = (is_array($value) || is_object($value)) ? $value : $config;`, no `array_merge`), so a
|
|
1321
|
+
tenant override **must restate `"role":"suggestion"`** beside its `items` or the chips **silently
|
|
1322
|
+
disappear**; a bare top-level array passes that check, which is why it is the tempting wrong move;
|
|
1323
|
+
the JSON goes in **`c_longValue`, never `value`** (varchar(255) overflow — resolver falls back at
|
|
1324
|
+
`Surface.php:907`), and `_castOverrideValue` `error_log`s + returns null on bad JSON; (3) the chip
|
|
1325
|
+
prompts live in **`config.items[]`**, not an `Actions` row — `RUN_TALOS` is in the ENUM but no
|
|
1326
|
+
`Actions` row has ever been seeded and there is no FE registry entry, while `config` is emitted
|
|
1327
|
+
verbatim; (4) a **purely additive, id-agnostic** seed may use **`AUTO_INCREMENT` + `NOT EXISTS`**
|
|
1328
|
+
instead of reserved literal ids — the discriminator is whether the file re-parents rows or a
|
|
1329
|
+
`Client_*` file must inline a Core id, and the analogue is the original `navigation` seed; (5)
|
|
1330
|
+
**OFF is correct, not just cautious**, for a feature that sends a tenant's live data to an external
|
|
1331
|
+
AI; (6) both files now carry a commented **TEARDOWN** whose order is load-bearing — client
|
|
1332
|
+
`SurfaceOverrides` first, then Core `SurfaceElements`, then `Surfaces` (the `talos.feature.name`
|
|
1333
|
+
`Message` is left behind on purpose). **FIXED prod-readiness** (developer caught it): Core and
|
|
1334
|
+
`Client_*` are on **separate clusters** in prod, so the client file's `JOIN Core.SurfaceElements`
|
|
1335
|
+
cannot run there — restructured into **Section A** (single-cluster join, local/beta only),
|
|
1336
|
+
**Section B** (read-only `SELECT` on the Core cluster printing ids + a paste-able `VALUES` body),
|
|
1337
|
+
**Section C** (literal-id guarded `INSERT` on the client cluster), with rollback/verify in both
|
|
1338
|
+
forms. **De-qualified the Core file** (`Core.Messages` → `Messages`) because api2 can rename the
|
|
1339
|
+
Core schema via `[database] core_dbname`, while the per-client file **keeps** its `Client_Nychh.`
|
|
1340
|
+
qualification so a wrong-cluster run fails loudly rather than granting another tenant the feature.
|
|
1341
|
+
DISCOVERED that **`SurfaceOverrides`' unique key protects a client-wide grant not at all** — MySQL
|
|
1342
|
+
treats NULLs in a unique key as distinct, and a client-wide grant is `personaId`/`roleId`/
|
|
1343
|
+
`languageId` all NULL, so the `NOT EXISTS` guard is the *only* thing making a re-run idempotent.
|
|
1344
|
+
Verified against the real DDL: `iconToken` nullable, `NEUTRAL`/`BUTTON`/`TEXT`/`LEFT` valid,
|
|
1345
|
+
`Surfaces.type DRAWER` exists, `Surfaces` UNIQUE KEY is `(slug, appId)`. Run state: **local only**
|
|
1346
|
+
(`navigation` = surface 39, appId 1). The first attempt consumed `SurfaceElements` **216–220** under
|
|
1347
|
+
the old per-element shape; those were torn down and re-created under the two-element shape, so local
|
|
1348
|
+
ids advanced again — **element ids are `AUTO_INCREMENT` and therefore per-environment**, which is
|
|
1349
|
+
why prod reads them off Core in Section B. **Beta and prod NOT yet run.** (apeterson)
|
|
1229
1350
|
- 2026-08-31 — Added the **denial-details** seed pair (`Core/2026-08-31a -
|
|
1230
1351
|
SalesOrderDenialDetailsSurfaceSeed.sql` — Surface **47**, elements **170–176**, Messages
|
|
1231
1352
|
**227–232**, plus a `readOnlyRule` marker on surface 41; and `Client_Compass/2026-08-31a -
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| [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/components/Table/themeConfig/toga.module.css, toga25-supply/src/themeConfig.json, 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 |
|
|
12
12
|
| [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 |
|
|
13
13
|
| [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/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, 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 |
|
|
14
|
-
| [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts, toga-blox/src/components/Talos/README.md |
|
|
14
|
+
| [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts, toga-blox/src/components/Talos/README.md, toga-blox/src/components/TableRecordModal/TableRecordModal.tsx |
|
|
15
15
|
| [Toaster — host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
|
|
16
16
|
| [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 |
|
|
17
17
|
| [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
|
|
@@ -6,7 +6,7 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx
|
|
@@ -62,6 +62,14 @@ Common (`AdvancedSelectBaseProps<T>`): `data: T[]`, `getOptionValue/getOptionLab
|
|
|
62
62
|
|
|
63
63
|
- **`renderOption` prop is declared but unused** — option rendering is hardcoded (primary +
|
|
64
64
|
secondary label, check/checkbox, avatar). Treat custom rendering as not yet supported.
|
|
65
|
+
- **⚠ GAP: there is NO footer / pinned-action slot.** A select that needs a persistent action in the
|
|
66
|
+
panel — the common "+ Add contact" when the option list is empty or wrong — cannot be built with
|
|
67
|
+
`AdvancedSelect` today. **And the obvious workaround does not work:** a sentinel option pushed into
|
|
68
|
+
`data` is **filtered out by the built-in search**, i.e. it disappears exactly when the user has
|
|
69
|
+
typed something and most needs it. Adding a real footer slot (rendered outside the virtualizer and
|
|
70
|
+
outside the search filter) is the fix; until then this pushes consumers to app-local selects.
|
|
71
|
+
Worked example: toga25-supply's app-local `PanelSelect` in the Create Transfer Order modal —
|
|
72
|
+
[Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
|
|
65
73
|
- Single-select with a `secondaryLabel` and menu closed shows a styled two-line value; the
|
|
66
74
|
native input collapses to width 0 (`.inputCollapsed`).
|
|
67
75
|
- Selected-float-to-top is **not** configurable.
|
|
@@ -70,4 +78,9 @@ Common (`AdvancedSelectBaseProps<T>`): `data: T[]`, `getOptionValue/getOptionLab
|
|
|
70
78
|
- No built-in required validation or full ARIA listbox/option roles (aria-labels on buttons only).
|
|
71
79
|
|
|
72
80
|
## Change history
|
|
81
|
+
- 2026-09-02 — Recorded a real **gap**: `AdvancedSelect` has **no footer / pinned-action slot**, and
|
|
82
|
+
a sentinel option placed in `data` is filtered out by the component's own search — so a select that
|
|
83
|
+
needs a persistent "+ Add …" action cannot use it. This is why toga25-supply built an app-local
|
|
84
|
+
`PanelSelect` for the Create Transfer Order modal instead of consuming blox. Suggested fix: a
|
|
85
|
+
footer slot rendered outside the virtualizer and outside the search filter. (apeterson)
|
|
73
86
|
- 2026-06-23 — Documented AdvancedSelect: virtualization, client/server search with abort, infinite scroll, keyboard nav, single/multi modes, and the unused `renderOption` caveat (apeterson).
|
|
@@ -6,7 +6,7 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/src/components/TableRecordModal/TableRecordModal.tsx
|
|
@@ -15,6 +15,8 @@ files:
|
|
|
15
15
|
related:
|
|
16
16
|
- ../architecture.md
|
|
17
17
|
- table.md
|
|
18
|
+
- talos-assistant.md
|
|
19
|
+
- ../../toga25-supply/features/talos-integration.md
|
|
18
20
|
---
|
|
19
21
|
|
|
20
22
|
## Summary
|
|
@@ -58,6 +60,29 @@ it opens the modal with `isOpen={!!selectedRow}` and renders the record inside `
|
|
|
58
60
|
(often using the same `tableViewFields` metadata to format fields). `utils/resolveModalType.tsx`
|
|
59
61
|
and `types.ts` are present but **empty/reserved** for a future field-type→renderer mapping.
|
|
60
62
|
|
|
63
|
+
## `data-blox-modal-open` — the root-element marker (2026-09-02)
|
|
64
|
+
|
|
65
|
+
While open, `TableRecordModal` sets **`data-blox-modal-open` on `document.documentElement`**. Other
|
|
66
|
+
blox CSS reacts to it — today the Talos launcher dims under `:root[data-blox-modal-open]` (see
|
|
67
|
+
[Talos assistant](talos-assistant.md)).
|
|
68
|
+
|
|
69
|
+
**Why an attribute on the root element and not a prop or context.** The things that must react to
|
|
70
|
+
"a modal is open" render **outside this component's subtree** — the Talos launcher lives in the app
|
|
71
|
+
**header**. No shared React state reaches it. Threading a prop down would have touched **every
|
|
72
|
+
modal layout in the consuming app**. This is the same boundary-crossing trick as the existing
|
|
73
|
+
`--talos-dock-width` custom property.
|
|
74
|
+
|
|
75
|
+
**It is a COUNTER, not a boolean.** Modals stack — a record modal can open a create modal — and the
|
|
76
|
+
marker must **survive the inner one closing**. So:
|
|
77
|
+
|
|
78
|
+
- Each instance holds a **`holdsModalMarker` ref**, so a re-render cannot double-count and a close
|
|
79
|
+
cannot decrement twice.
|
|
80
|
+
- The marker is cleared at **three** points: the open→false transition's **animation end**, an
|
|
81
|
+
**unmount** safety net, and the **count reaching 0**.
|
|
82
|
+
|
|
83
|
+
Anything else that reads the marker gets stacking correctness for free. (Note this fixes stacking
|
|
84
|
+
for the *marker* only — the body-overflow gotcha below is unchanged.)
|
|
85
|
+
|
|
61
86
|
## Gotchas
|
|
62
87
|
|
|
63
88
|
- **No portal** — renders inline in the React tree; a parent `overflow: hidden` can clip it.
|
|
@@ -66,4 +91,12 @@ and `types.ts` are present but **empty/reserved** for a future field-type→rend
|
|
|
66
91
|
- CSS-variable defaults are hardcoded fallbacks; set vars on the DOM before mount to change sizes.
|
|
67
92
|
|
|
68
93
|
## Change history
|
|
94
|
+
- 2026-09-02 — Added **`data-blox-modal-open` on `document.documentElement`** while a modal is open,
|
|
95
|
+
so chrome rendered **outside** the modal subtree can react (first consumer: the Talos launcher dims
|
|
96
|
+
under `:root[data-blox-modal-open]`). Chosen over a prop/context because the launcher lives in the
|
|
97
|
+
app **header** — no shared React state reaches it, and threading a prop would touch every modal
|
|
98
|
+
layout in the host. It is a **counter, not a boolean**, because modals stack and the marker must
|
|
99
|
+
survive the inner one closing; each instance holds a `holdsModalMarker` ref so a re-render cannot
|
|
100
|
+
double-count and a close cannot decrement twice, and the marker clears at three points (open→false
|
|
101
|
+
animation end, unmount safety net, count reaching 0). (apeterson)
|
|
69
102
|
- 2026-06-23 — Documented TableRecordModal as a generic modal shell: positions, render-prop children, body-scroll lock, and consumer-owned field rendering / a11y (apeterson).
|
|
@@ -21,7 +21,9 @@ files:
|
|
|
21
21
|
- toga-blox/src/components/Talos/TalosPanel.test.tsx
|
|
22
22
|
- toga-blox/src/components/index.ts
|
|
23
23
|
- toga-blox/src/components/Talos/README.md
|
|
24
|
+
- toga-blox/src/components/TableRecordModal/TableRecordModal.tsx
|
|
24
25
|
related:
|
|
26
|
+
- table-record-modal.md
|
|
25
27
|
- ../architecture.md
|
|
26
28
|
- ../workflows/dynamic-publish-pipeline.md
|
|
27
29
|
- ../workflows/local-link-into-a-consumer-app.md
|
|
@@ -254,9 +256,42 @@ threadsStatus?: "loading" | "error" | "ready";
|
|
|
254
256
|
`DEFAULT_SUGGESTIONS` — four hardcoded **TOGa Supply** questions — was **removed**. Tenant
|
|
255
257
|
business copy must not sit inside a library five apps consume: any host that mounted the panel
|
|
256
258
|
silently inherited Supply's prompts. `suggestions` now defaults to an **empty module-level
|
|
257
|
-
constant**, and the host supplies the chips (supply reads them from the surface layer
|
|
259
|
+
constant**, and the host supplies the chips (supply reads them from the surface layer as **one
|
|
260
|
+
authored, ordered set per tenant** — see
|
|
258
261
|
[Talos integration](../../toga25-supply/features/talos-integration.md)).
|
|
259
262
|
|
|
263
|
+
### The launcher is DISABLED under an open modal — and the cause was NOT z-index (2026-09-02)
|
|
264
|
+
|
|
265
|
+
The launcher stayed bright and looked clickable while a record modal was open. The obvious diagnosis
|
|
266
|
+
(and the developer's first guess) was z-index. **It was wrong.** Checked, not assumed:
|
|
267
|
+
|
|
268
|
+
- `.launcher` has **no `z-index`** at all.
|
|
269
|
+
- blox `.header` has **no `z-index` and no `position`**; supply's `.headerActions` has neither.
|
|
270
|
+
- The modal `.backdrop` is `position: fixed; z-index: 40`.
|
|
271
|
+
|
|
272
|
+
So the launcher was **already painted under the backdrop and its clicks were already being
|
|
273
|
+
swallowed by it.** The real cause is purely visual: `.backdrop-right` / `.backdrop-left` is only
|
|
274
|
+
`rgba(0, 0, 0, 0.2)`, and a 20% black wash over a **white** button still reads as active next to
|
|
275
|
+
dimmed header chrome.
|
|
276
|
+
|
|
277
|
+
**Fix — a real disabled state, not a z-index change.** `TableRecordModal` sets
|
|
278
|
+
**`data-blox-modal-open` on `document.documentElement`**, and `Talos.module.css` dims the launcher
|
|
279
|
+
under `:root[data-blox-modal-open]`: `opacity: .5`, `filter: grayscale(1)`,
|
|
280
|
+
`pointer-events: none`, plus **stopping the `.launcher::before` orbit animation** (a spinning ring
|
|
281
|
+
on a greyed-out button still reads as live). Marker mechanics and why it is a **counter**:
|
|
282
|
+
[TableRecordModal](table-record-modal.md).
|
|
283
|
+
|
|
284
|
+
This applies to **every** `TableRecordModal` in a host app automatically.
|
|
285
|
+
|
|
286
|
+
🚨 **Known gap, left unfixed.** The launcher is still **keyboard-reachable** behind a modal —
|
|
287
|
+
`pointer-events: none` stops the **mouse only**. Supply's header takes `inert` for the side nav but
|
|
288
|
+
not for modals, and closing that is a **host** change affecting all header controls, so it was
|
|
289
|
+
flagged to the developer rather than patched here.
|
|
290
|
+
|
|
291
|
+
**Reusable lesson:** before reaching for z-index, read the actual computed stack. "Looks clickable"
|
|
292
|
+
and "is clickable" are different bugs, and a translucent scrim is a *contrast* problem, not a
|
|
293
|
+
*stacking* one.
|
|
294
|
+
|
|
260
295
|
### Tests (first ever for this component)
|
|
261
296
|
|
|
262
297
|
`TalosPanel.test.tsx` — **31 Vitest + RTL Talos tests** as of 2026-09-02 (8 at inception), all
|
|
@@ -324,6 +359,20 @@ Rollup *"failed to resolve import"* for a component they never referenced). blox
|
|
|
324
359
|
currently consumes it via a local symlink.
|
|
325
360
|
|
|
326
361
|
## Change history
|
|
362
|
+
- 2026-09-02 — **FIXED the launcher staying bright and clickable-looking under an open record
|
|
363
|
+
modal — and the cause was NOT z-index.** `.launcher` has no `z-index`, blox `.header` has no
|
|
364
|
+
`z-index`/`position`, and the modal `.backdrop` is `fixed; z-index: 40`, so the launcher was
|
|
365
|
+
**already** behind the backdrop with its clicks **already** swallowed. The real cause was contrast:
|
|
366
|
+
`.backdrop-right`/`-left` is only `rgba(0,0,0,0.2)` and a 20% wash over a white button still reads
|
|
367
|
+
as active. Fix is a real disabled state — `TableRecordModal` sets **`data-blox-modal-open` on
|
|
368
|
+
`document.documentElement`** (a **counter**, since modals stack) and `Talos.module.css` applies
|
|
369
|
+
`opacity: .5` / `grayscale(1)` / `pointer-events: none` and **stops the `.launcher::before` orbit
|
|
370
|
+
animation** under `:root[data-blox-modal-open]`. A root attribute rather than a prop because the
|
|
371
|
+
launcher renders in the host **header**, outside the modal subtree — the same trick as
|
|
372
|
+
`--talos-dock-width`. Applies to every `TableRecordModal` automatically. **Known gap flagged, not
|
|
373
|
+
fixed:** the launcher is still **keyboard**-reachable behind a modal; `pointer-events: none` stops
|
|
374
|
+
the mouse only, and adding `inert` is a host-header change. Suite at **31 Talos + TableRecordModal
|
|
375
|
+
tests**; blox `dist/` rebuilt. (apeterson)
|
|
327
376
|
- 2026-09-02 — **FIXED the `renderMessages` slot silently killing the in-flight loading UI**: the
|
|
328
377
|
bubble is synthesized from `isStreaming`/`progress` into the panel's internal `msgs` array, and the
|
|
329
378
|
host-render branch replaced that whole array, so a run showed **no feedback between submit and
|