toga-ai 1.0.1076 → 1.0.1077
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 +2 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +5 -1
- package/knowledge/2.0/apps/_underscore/features/autoincrement-record-numbering.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/global-search.md +54 -0
- package/knowledge/2.0/apps/_underscore/features/notification-engine.md +65 -0
- package/knowledge/2.0/apps/_underscore/features/record-change-audit-log.md +3 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +4 -3
- package/knowledge/2.0/apps/api2/features/language-translation-layer.md +5 -3
- package/knowledge/2.0/apps/api2/features/record-scripts.md +5 -3
- package/knowledge/2.0/apps/dbchanges2/architecture.md +2 -0
- package/knowledge/2.0/apps/toga25-blox/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-blox/features/shell-search-and-notifications.md +44 -0
- package/knowledge/2.0/standards/backend-php.md +3 -1
- package/knowledge/2.0/standards/frontend.md +3 -1
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
| [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` columns are hydrated lazily per returned row, so a missing column 500s only when a query returns rows; open when auditing storage-field schema d |
|
|
36
36
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time webhook importer that writes NetSuite sale records (invoice/cashSale/creditMemo/cashRefund/journalEntry) into `Forecast.Sales`; open before touching s |
|
|
37
37
|
| [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | api2 interceptor that propagates `Items.isFulfillable` UP the SO↔PO chain onto client-facing item rows; open when the storefront Qty-Fulfilled gate is wrong or |
|
|
38
|
+
| [Global search (GET /records/search — tiered, per-client, data-configured fields)](features/global-search.md) | Platform (all 2.0 apps) header search across record types; open when making a field searchable, tuning ranking, or a search is slow or misses results. |
|
|
38
39
|
| [ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)](features/item-classification-itemclasses.md) | `ItemClasses` is a near-unused per-client lookup that 2 of 36 clients already repurpose (Northwell service categories, Prudential device tiers); open before reu |
|
|
39
40
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Item-fulfillment stage lifecycle (picked→packed→shipped) driving the calculated order `_status`, with per-client status policies; open when working order status |
|
|
40
41
|
| [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | DB-free unit-test scaffold for `_underscore` model interceptor/validation logic (stub the persistence base, reflect into privates); open when testing a `prePost |
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | Shared 2.0 `_underscore` NetSuite REST client (`_Component_Api_Netsuite`) — record GET/write, SuiteQL, auth, escaping, logging; open before any 2.0 NetSuite cal |
|
|
45
46
|
| [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | Where the shared NetSuite SO importer (`_Trait_Netsuite_SalesOrder`) sources SO#, PO#, and ship-to phone, and the traps in changing it; open before editing this |
|
|
46
47
|
| [Note records have NO author — every `*Notes` model's `contactId` is unwritten](features/note-record-authorship.md) | Every `*Notes` model declares a `contactId` FK but no code writes it — note authorship is unrecoverable from 2.0 data; open before building any note author disp |
|
|
48
|
+
| [Notification engine (rule-driven in-app notifications from record change logs)](features/notification-engine.md) | Platform (all 2.0 apps) in-app notifications: data rules on Core turn a record's change log into Client.Notifications rows; open when adding a notification kind |
|
|
47
49
|
| ["Order For" delegation (Users_DelegatorUsers, the permission gate, originalUserId)](features/order-for-delegation.md) | Server side of "Order For" (TRUE-81848): who may order for whom, where that is enforced, and how the audit column records it. |
|
|
48
50
|
| [Outbound third-party API rate budget (_Component_Api_RateBudget) — designed + staged, NOT merged](features/outbound-api-rate-budget.md) | Design record for a shared outbound API rate budget (one component + one Core table, keyed by `bucketKey`); open when adding call-rate metering/limiting to a 2. |
|
|
49
51
|
| [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | Legacy `Page::meta()` page-meta resolver + context-scoped `ClientRecordFieldSettings`; open when a grid column header is wrong/camelCase, or a `/pages/meta` pag |
|
|
@@ -10,6 +10,7 @@ updated: 2026-10-18
|
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Component/Acl/ReadExpression/ReadExpression.php
|
|
13
14
|
- dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql
|
|
14
15
|
- _underscore/Model/Core/Page.php
|
|
15
16
|
- _underscore/Model/Core/Surface.php
|
|
@@ -578,7 +579,9 @@ INSERT INTO AclLogicGroupExpressions SET
|
|
|
578
579
|
`_Component_Api_V2::buildLookups()` bulk-loads these tables (from the Core and/or Client ACL
|
|
579
580
|
database per `Records.aclDatabase`) into in-memory lookups once per request; `processRoutePairs()`
|
|
580
581
|
then checks the caller's roles against `AclRecordPermissions` (→ `EZ-1` if no grant) and computes
|
|
581
|
-
readable/writable fields from `AclFieldPermissions`.
|
|
582
|
+
readable/writable fields from `AclFieldPermissions`. The row-level READ expression is built by
|
|
583
|
+
`_Component_Acl_ReadExpression` (`_underscore`, moved out of V2 2026-10-18) — shared by V2 and the
|
|
584
|
+
[notification engine](./notification-engine.md); change it in one place. `_Model_Core_Page::meta()` reads the same
|
|
582
585
|
tables to return per-page ACL to the frontend.
|
|
583
586
|
|
|
584
587
|
> **⚠ There is NO ACL/metadata caching layer — the load is per-request and there is nothing to bust.**
|
|
@@ -937,6 +940,7 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
937
940
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
938
941
|
|
|
939
942
|
## Change history
|
|
943
|
+
- 2026-10-18 — Row-level READ expression moved to shared `_Component_Acl_ReadExpression` (V2 + notification engine). TRUE-82907 (jcardinal)
|
|
940
944
|
- 2026-10-18 — Client ACL rows now carry `capabilityId`; linked the new Capabilities doc (TRUE-82880). (jcardinal)
|
|
941
945
|
- 2026-09-21 — Named the mechanism behind the half-built chain: `_Model_Core_AclRecordPermission::buildSqlExpression`
|
|
942
946
|
returns **`'(0)'`** with no logic group, so `allowRead = 1` alone denies everything (second symptom: 403 listing
|
|
@@ -58,7 +58,8 @@ Removed: `FIELDOPT_AUTOINCREMENT_PREFIX` / `_FIRSTNUMBER` / `_SUFFIX`, the `MAX(
|
|
|
58
58
|
|
|
59
59
|
## Gotchas
|
|
60
60
|
|
|
61
|
-
- **New numbered model needs a `RecordNumbers` row in every client DB** (or Core for `DB_CORE` models), else creates throw.
|
|
61
|
+
- **New numbered model needs a `RecordNumbers` row in every client DB** (or Core for `DB_CORE` models), else creates throw. Example: Scopes (record 356) = prefix `SC`, `nextNumber` 100020 (`Client/2026-10-18e`); `Model/Client/Scope.php` and `Model/Core/Client.php` are plain `FIELD_AUTOINCREMENT`.
|
|
62
|
+
- **Prefix = letters only.** `SC-` is invalid (no hyphen) — the old `SC-100001` style is gone.
|
|
62
63
|
- **New client DB** must get `RecordNumbers` rows from the `Client/` blank seed.
|
|
63
64
|
- **Gaps only come from failed saves.** A gap = a failed create → check `Logs.Api` / `Logs.Issue`.
|
|
64
65
|
- **SA / TR numbers are placeholders** — NetSuite `tranId` overwrites them on write-back (accepted). Never key a match on `number`; pushes key `externalId` on uuid.
|
|
@@ -79,6 +80,7 @@ Removed: `FIELDOPT_AUTOINCREMENT_PREFIX` / `_FIRSTNUMBER` / `_SUFFIX`, the `MAX(
|
|
|
79
80
|
- [NetSuite SalesOrder outbound push](../../worker2/features/netsuite-salesorder-outbound-push.md)
|
|
80
81
|
|
|
81
82
|
## Change history
|
|
83
|
+
- 2026-10-18 — `_production` merged into `toga25`; Scope + Core Client models on plain `FIELD_AUTOINCREMENT`; Scopes seeded `SC` 100020. TRUE-82907 (jcardinal)
|
|
82
84
|
- 2026-10-18 — Final design: per-client `RecordNumbers` table (client DBs + Core), counting in order, old series continued under new prefixes, Compass Canada SAC; dropped the platform-wide scrambled `Core.Records` counter. TRUE-82715. (jcardinal)
|
|
83
85
|
- 2026-10-13 — Platform-wide scrambled counter in `Core.Records` (dropped before release). (jcardinal)
|
|
84
86
|
- 2026-10-02 — Per-tenant Parameters counter on `_beta` (superseded, PR 756 not merged). (rgirish)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Global search (GET /records/search — tiered, per-client, data-configured fields)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-18
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Component/GlobalSearch/GlobalSearch.php
|
|
13
|
+
- _underscore/Model/Core/Record.php
|
|
14
|
+
- _underscore/Model/Core/SearchField.php
|
|
15
|
+
- _underscore/Model/Cache/SearchRequest.php
|
|
16
|
+
- _underscore/Component/RecordActivity/Reader/Reader.php
|
|
17
|
+
related:
|
|
18
|
+
- ../../api2/features/record-scripts.md
|
|
19
|
+
- ./autoincrement-record-numbering.md
|
|
20
|
+
- ./notification-engine.md
|
|
21
|
+
- ../../toga25-blox/features/shell-search-and-notifications.md
|
|
22
|
+
---
|
|
23
|
+
Platform (all 2.0 apps) header search across record types; open when making a field searchable, tuning ranking, or a search is slow or misses results.
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
`GET /records/search` is a Record Script on record 6 (`Records`), `phpMethod` `globalSearch`. Searchable fields are rows in `Core.SearchFields` — no code per record type. The browser calls each client it can reach, with that client's own token. Built TRUE-82907.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
**Tiers** (one call = one tier):
|
|
32
|
+
1. tier 1 = EXACT on tier-1 fields,
|
|
33
|
+
2. tier 2 = STARTS WITH on fields with tier <= 2,
|
|
34
|
+
3. tier 3 = CONTAINS on fields with tier <= 3.
|
|
35
|
+
|
|
36
|
+
A field is skipped when its `maxTier` < the requested tier.
|
|
37
|
+
|
|
38
|
+
**`Core.SearchFields`:** `recordFieldId`, `tier` (TINYINT UNSIGNED), `maxTier`, `minChars`, `parentRecordFieldId`, `sortOrder`.
|
|
39
|
+
|
|
40
|
+
**Number hint.** A term matching `^[A-Z]{1,8}\d+$` whose prefix exists in that client's `RecordNumbers` searches only that record type's number field ([record numbering](./autoincrement-record-numbering.md)).
|
|
41
|
+
|
|
42
|
+
**Reads** go through api2 `Reader::listAsReader` — the caller's record + field ACL apply. Not `internalApiRequest` (that writes a `Logs.Api` row and commits per call).
|
|
43
|
+
|
|
44
|
+
**Cancel stale searches.** `Cache.SearchRequests` keeps one row per client + user + search box with the latest sequence number. The search stops between record types when a newer sequence number exists.
|
|
45
|
+
|
|
46
|
+
**Rank:** exact match, all words, whole word, fewer results of that type, newest.
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **A field may be tier 1 or 2 only if it has its own index (as the first column).** Otherwise exact/starts-with scans the table. Indexes added: `Customers.name`, `Locations.name`, `Units.serialNumber`, `Users.firstName`, `Users.lastName` (`Client/2026-10-18g`).
|
|
51
|
+
- `phpMethod` is `globalSearch`, not `search` — see [record scripts](../../api2/features/record-scripts.md) gotcha on `_Model` method names.
|
|
52
|
+
|
|
53
|
+
## Change history
|
|
54
|
+
- 2026-10-18 — Built tiered global search, `Core.SearchFields`, `Cache.SearchRequests`, search indexes. TRUE-82907 (jcardinal)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Notification engine (rule-driven in-app notifications from record change logs)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-18
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Notification.php
|
|
13
|
+
- _underscore/Component/Notification/Dispatcher/Dispatcher.php
|
|
14
|
+
- _underscore/Component/Acl/ReadExpression/ReadExpression.php
|
|
15
|
+
- _underscore/Component/SettingResolver/SettingResolver.php
|
|
16
|
+
- _underscore/Model/Core/NotificationType.php
|
|
17
|
+
- _underscore/Model/Core/NotificationRule.php
|
|
18
|
+
- _underscore/Model/Client/Notification.php
|
|
19
|
+
- api2/Component/Api/V2/V2.php
|
|
20
|
+
- worker2/Worker/Notification/Dispatch.php
|
|
21
|
+
related:
|
|
22
|
+
- ./record-change-audit-log.md
|
|
23
|
+
- ./acl-permission-chain.md
|
|
24
|
+
- ./global-search.md
|
|
25
|
+
- ../../api2/features/language-translation-layer.md
|
|
26
|
+
- ../../toga25-blox/features/shell-search-and-notifications.md
|
|
27
|
+
- ../../worker2/features/creating-worker-actions.md
|
|
28
|
+
---
|
|
29
|
+
Platform (all 2.0 apps) in-app notifications: data rules on Core turn a record's change log into Client.Notifications rows; open when adding a notification kind/rule, a non-V2 writer must notify, or a user did/didn't get a notification.
|
|
30
|
+
|
|
31
|
+
## Summary
|
|
32
|
+
|
|
33
|
+
Setup is data, not code: one `Core.NotificationTypes` row (what it looks like) + one `Core.NotificationRules` row (when it fires, who gets it). Any record written through api2 V2 then notifies automatically. Other writers call `_Notification::dispatchForLogRecords($clientId, $logRecordIds)` after their own commit. Built TRUE-82907.
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
1. **Trigger (api2).** After a successful V2 POST/PUT/PATCH, `flushPendingRecordChangeLogs()` commits the change logs ([audit log](./record-change-audit-log.md)). After that commit it keeps only `Logs.Record` ids whose `recordId` has an active `NotificationRules` row (`_Notification::selectLogRecordIdsWithActiveRules`) and sends ONE `_Worker::runTask('Notification/Dispatch/Run', ['clientId'=>…, 'logRecordIds'=>[…]])` per request, chunks of 500. A dispatch failure is logged and never fails the user's request.
|
|
38
|
+
2. **Worker.** worker2 `_Worker_Notification_Dispatch::Run` → `_Component_Notification_Dispatcher`. Reads `Logs_<client>.Record` + `RecordField` (old/new values).
|
|
39
|
+
3. **Match rules:** `action` CREATE / UPDATE; optional `changedRecordFieldId` (fires only when that field changed).
|
|
40
|
+
4. **Recipients:**
|
|
41
|
+
- `FIELD` — a person field on the record. A `Contacts` FK is resolved to `Users.contactId`.
|
|
42
|
+
- `RELATED` — a user field on a related table via `viaRecordFieldId` (e.g. `TicketWatchers`).
|
|
43
|
+
- Optional `parentRecordFieldId` — child record (e.g. `TicketNotes`) notifies about and links to the parent (the ticket).
|
|
44
|
+
5. **Filter recipients:** drop the actor; drop users without READ on the record — row-level ACL through `_Component_Acl_ReadExpression` (the same builder V2 uses; see [ACL chain](./acl-permission-chain.md)); drop users whose setting is off.
|
|
45
|
+
6. **User setting.** `NotificationTypes.settingId` NULL = always on. Otherwise resolved through the `*Settings` cascade by `_Component_SettingResolver` (same resolver as language/currency). Values `'0'`, `'false'`, `'off'`, `'no'` = off; unset = on.
|
|
46
|
+
7. **Insert** `Client.Notifications`: `notificationTypeSlug`, `messageParams` JSON snapshot (number / title / subtitle), `actorUserId`, `logRecordId`, `primaryKeyUuid`, `dtCreated`, `dtUpdated`, `dtCleared`. `UNIQUE(logRecordId, userId, notificationTypeSlug)` blocks duplicates from SQS re-delivery.
|
|
47
|
+
|
|
48
|
+
**Data model (dbchanges2 `Core/2026-10-18h..l`, `Client/2026-10-18e..g`):**
|
|
49
|
+
- `Core.NotificationTypes` — `slug`, `iconToken`, `colorToken`, title/body `Core.Messages` keys on the app-less Surface `notifications`, `settingId`.
|
|
50
|
+
- `Core.NotificationRules` — record, action, optional changed field, recipient kind + field, `viaRecordFieldId`, `parentRecordFieldId`, active flag.
|
|
51
|
+
- `Client.Notifications` references the type by **slug** — `NotificationTypes` ids are auto-increment and differ per environment.
|
|
52
|
+
- Clear = set `dtCleared` (archive). Never delete.
|
|
53
|
+
- ACL is owner-only: `Notifications.userId = AclUser.id` + `AclApi.id IS NULL` (same pattern as `TicketWatchers`).
|
|
54
|
+
- `_Model_Client_Notification` has calc field `_isMine`.
|
|
55
|
+
|
|
56
|
+
## Gotchas
|
|
57
|
+
|
|
58
|
+
- **Only V2 writes notify automatically.** A worker, cron, or model writing records outside V2 must call `_Notification::dispatchForLogRecords()` itself, after its own commit — or nobody is notified.
|
|
59
|
+
- **Fail closed on per-app-only READ grants.** A user whose READ comes only from an app-scoped grant is never notified (the worker has no app context).
|
|
60
|
+
- **Local dispatch fails (known gap).** In debug mode `_Worker::runTask` POSTs to `_Config::api('worker')`; dev configs have no worker URL and worker2 has no dev config. Locally the user request still succeeds; no notification rows appear.
|
|
61
|
+
- **Existing writers keep working.** Compass's own `Notifications` writer and the toga2-commerce bell are unchanged; they do not use rules.
|
|
62
|
+
- `_Component_Acl_ReadExpression` and `_Component_SettingResolver` were moved out of V2 unchanged (READ expression parity proven on 440 user/record pairs). Change them in one place — V2 and the dispatcher both depend on them.
|
|
63
|
+
|
|
64
|
+
## Change history
|
|
65
|
+
- 2026-10-18 — Built the rule-driven engine, Notifications archive columns, owner-only ACL; moved READ expression + setting resolver into `_underscore`. TRUE-82907 (jcardinal)
|
|
@@ -20,6 +20,7 @@ related:
|
|
|
20
20
|
- ../../../../clients/compass-usa/features/stranded-approval-reassignment.md
|
|
21
21
|
- ../../api2/features/api-payload-interceptors.md
|
|
22
22
|
- ../../worker2/features/creating-worker-actions.md
|
|
23
|
+
- ./notification-engine.md
|
|
23
24
|
---
|
|
24
25
|
|
|
25
26
|
Field-level change audit trail per tenant; open to answer "who changed this field, to what, when" or to tell a direct SQL edit from an in-app change.
|
|
@@ -30,6 +31,8 @@ Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. `Log
|
|
|
30
31
|
|
|
31
32
|
Key property: **a change made outside the API leaves a `Record` row's field children empty — or no row at all.** That absence distinguishes a direct SQL edit from an in-app change — usually the first fork in any "how did this record get like this?" investigation.
|
|
32
33
|
|
|
34
|
+
These rows also drive in-app notifications: after api2 commits them, matching rows go to the [notification engine](./notification-engine.md).
|
|
35
|
+
|
|
33
36
|
This is the **model-write** audit log — a different thing from the **request** log (`Logs_<Client>.Api` / core `Logs.Api`, see [V2 Request Logging](../../api2/features/request-logging.md), which records HTTP traffic; this one records the resulting field mutations).
|
|
34
37
|
|
|
35
38
|
## How it works
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: [jcardinal, apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Language.php
|
|
@@ -323,8 +323,8 @@ Same discipline as the reserved-id blocks: **read the id out of the target envir
|
|
|
323
323
|
|
|
324
324
|
Developer decision (jcardinal, 2026-10-08), platform apps (Desk, IQ, template). Which layer owns what:
|
|
325
325
|
|
|
326
|
-
1. **Surface owns what users SEE.** `Core.Surfaces` / `SurfaceElements` / `Messages` + Client `MessageTranslations` + Client `SurfaceOverrides`: copy, labels, order, visibility, required, icons,
|
|
327
|
-
2. **`*Settings` keeps ONLY preferences:** `language`, `date-format`, `time-format`, `time-zone`, `currency`, `persona`. One shared preferences resolver (
|
|
326
|
+
1. **Surface owns what users SEE.** `Core.Surfaces` / `SurfaceElements` / `Messages` + Client `MessageTranslations` + Client `SurfaceOverrides`: copy, labels, order, visibility, required, icons, colors. Client changes = `SurfaceOverrides` (client / persona / role); translations = `MessageTranslations`.
|
|
327
|
+
2. **`*Settings` keeps ONLY preferences:** `language`, `date-format`, `time-format`, `time-zone`, `currency`, `persona`. One shared preferences resolver, `_Component_SettingResolver` in `_underscore` (moved out of api2 V2 2026-10-18), honoring `isOverridable`. UI keys are FROZEN, new apps never read them: `label`, `label-singular`, `icon`, `tooltip`, `placeholder`, `sort-order`, `is-required`, `default-value`, `min-required-siblings`, `max-required-siblings`.
|
|
328
328
|
3. **Record + field names move to `Core.Messages`** (translatable) via message-id link columns on `Core.Records` / `Core.RecordFields`, first seeded from `DefaultRecordSettings` / `DefaultRecordFieldSettings` labels; until seeded, fall back to the settings label.
|
|
329
329
|
4. **No per-user text overrides.** User choices = preferences, saved views, column order. Add a Surface lock only when a real need appears.
|
|
330
330
|
5. **`SurfaceOverrides` keep numeric Core ids** in client DBs (no uuid re-key) - see [surface-overrides-uuid-rekey](surface-overrides-uuid-rekey.md).
|
|
@@ -344,5 +344,6 @@ Precedence is per layer and never mixed on one value: Surface = base < client <
|
|
|
344
344
|
- [side-navigation-and-default-route](../../toga25-supply/features/side-navigation-and-default-route.md)
|
|
345
345
|
|
|
346
346
|
## Change history
|
|
347
|
+
- 2026-10-18 — Shared preferences resolver now lives in `_underscore` as `_Component_SettingResolver`. TRUE-82907 (jcardinal)
|
|
347
348
|
- 2026-10-17 — `IS_REQUIRED` override loses to `requiredRule`; use static required + `visibilityRule` (TRUE-82889). (jcardinal)
|
|
348
349
|
- 2026-10-08 — Added "Layer boundary": Surface owns what users see, `*Settings` = preferences only (jcardinal decision; see Desk ADR 0140) (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-18
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -37,8 +37,9 @@ Uniform across all clients (no client variations). The sidecar table + ACL ship
|
|
|
37
37
|
## How it works
|
|
38
38
|
|
|
39
39
|
**Key files / entry points:**
|
|
40
|
-
- `_underscore/Model/Core/Setting.php` — `SLUG_LANGUAGE` const + `valueIsSet()` helper
|
|
41
|
-
- `
|
|
40
|
+
- `_underscore/Model/Core/Setting.php` — `SLUG_LANGUAGE` const + `valueIsSet()` helper.
|
|
41
|
+
- `_underscore/Component/SettingResolver/SettingResolver.php` — `_Component_SettingResolver`, the 8-layer Settings-cascade resolver (moved out of V2 unchanged 2026-10-18; V2 `resolveSettingValue()` delegates to it; also used by the [notification engine](../../_underscore/features/notification-engine.md)).
|
|
42
|
+
- `api2/Component/Api/V2/V2.php`: `resolveValidLanguageCode()` (validates against `Languages.code`, default `en`); `getTranslatedFieldValue()` / `loadTranslationSidecar()` / `shouldTranslate()` / `addTranslationFallbackWarning()` (read path); `extractTranslationWrites()` / `saveTranslationWrites()` (write path); `lookupTranslatableFieldByRecordFieldId` built in the RecordFields load (~line 200).
|
|
42
43
|
- `_underscore/Model/Client/ItemTranslation.php` — the first sidecar model.
|
|
43
44
|
|
|
44
45
|
**Language resolution (auth time, user auth only).** `resolveSettingValue('language', …)` walks the Settings Matrix in order — DefaultGlobal, DefaultApp, ClientGlobal, ClientApp, PersonaGlobal, PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Client/Persona layer with `isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first persona** (in the user's persona order) that sets it wins. The resolved code is validated against the client `Languages.code` (see *Invalid saved language* below), embedded in the JWT `id.language` claim, and echoed on every response as `audience.language`. API-credential auth gets no language.
|
|
@@ -113,4 +114,5 @@ Placement is load-bearing: the `id.language` mutation happens **after** client-D
|
|
|
113
114
|
- [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
|
|
114
115
|
|
|
115
116
|
## Change history
|
|
117
|
+
- 2026-10-18 — Settings-cascade resolver moved to `_Component_SettingResolver` (`_underscore`), unchanged. TRUE-82907 (jcardinal)
|
|
116
118
|
- 2026-10-09 — Login no longer fails on an unknown saved language (EV-16 retired): client-language fallback + WZ-4; language row made overridable on all clients; user-global-settings write guard linked. (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["kyalamarthi", "mhammontree", "tcox"]
|
|
9
|
+
updated: 2026-10-18
|
|
10
|
+
owners: ["kyalamarthi", "mhammontree", "tcox", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Team/Sprint.php
|
|
@@ -111,11 +111,13 @@ So for a scripted call, **authorization comes solely from `AclCustomRecordScript
|
|
|
111
111
|
- **No bound params in `_Query`.** Sanitize with `\_Database::escape()` (strings) and `(int)` casts (numerics) for every value placed into the query text — query-string args are user input.
|
|
112
112
|
- **Restore the query-cache flag.** `useQueryCache(false)` is a global toggle; capture the prior value and restore it, or you disable caching for the rest of the request.
|
|
113
113
|
- **`aclDatabase` has no `TEAM`.** A `Team`-schema record registers as `'CORE'`; picking `'CLIENT'` sends the engine looking in the wrong cluster.
|
|
114
|
-
- **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment.
|
|
114
|
+
- **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.** Concrete case (verified on production 2026-08-13): `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next free; prod max was 24"), and its sibling `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id 30** in production, so the grants pointed at nothing and a later file (`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that references a stale id inserts a row happily and fails **only** at request time as a missing-grant error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always verify the actual id in the target environment. **In a `Client/` grant file the Core lookup is not allowed (other cluster)**, so the file types the id as a literal with header SELECTs to run on Core; the operator replaces the literal per environment before running (approved manual step, jcardinal 2026-10-18).
|
|
115
|
+
- **⚠ `phpMethod` must not reuse a `_Model` method name.** `search` on `_Model_Core_Record` fatals ("Cannot make non static method … static") on every request that loads Records — `_Model` already has non-static `search()`. Global search uses `globalSearch` ([global search](../../_underscore/features/global-search.md)).
|
|
115
116
|
- **Registration is per-environment and split.** A missing `Core.RecordScripts` row → the route isn't a script (falls through to normal CRUD / 404-shaped behavior); a missing `AclRecordScripts` grant → the caller's roles can't run it. Both rows must exist in every environment the API serves.
|
|
116
117
|
- **The committed-tile definition is intentionally not the sprint-scoring definition.** `committedTile()` uses the **Power BI / current-state** definition of done/committed (`dtDone IS NOT NULL` + `workTypeNow`), which is *deliberately different* from TOGa Talos's canonical sprint scoring (`statusNow IN (STATUS_IN__DONE)` + `workTypeAtLock`). See [Team Sprint Management](../../worker2/features/team-sprint-management.md) before assuming a tile agrees with sprint scores.
|
|
117
118
|
|
|
118
119
|
## Change history
|
|
120
|
+
- 2026-10-18 — `phpMethod` vs `_Model` method-name collision; manual per-environment `RecordScripts` id in `Client/` grant files. TRUE-82907 (jcardinal)
|
|
119
121
|
- 2026-09-25 — renamed product references from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
|
|
120
122
|
|
|
121
123
|
## Related
|
|
@@ -411,9 +411,11 @@ The extra `SELECT … FROM ( … ) existing` layer both dodges error 1093 and pi
|
|
|
411
411
|
- **A `Client/` fan-out file that hardcodes a `customRecordFieldId` wires the wrong field for 29 of 30 tenants with no error** (rule #10). Custom-field ids are per-client `AUTO_INCREMENT`; keep custom-field work in the tenant's own folder.
|
|
412
412
|
- **An unguarded registration/seed insert can double-register** on a fresh environment (rule #7) — guard on the semantic identity, not the description.
|
|
413
413
|
- **The `dbchanges2` folder name IS the target database.** Running a `Core/` file against a client DB fails as `Table 'Client_X.<Table>' doesn't exist` (files are unqualified by design) — which names a missing table, not the real mistake. See [targeting the right database](workflows/targeting-the-right-database.md).
|
|
414
|
+
- **Core legacy tables are `utf8mb4_unicode_ci`; new tables are `utf8mb4_0900_ai_ci`.** Legacy = `Settings`, `DefaultGlobalSettings`, `Messages`, `Records`, `RecordFields`, `Surfaces`, `SurfaceElements`. Any SQL that compares strings between a legacy and a new table needs `COLLATE utf8mb4_unicode_ci` on the comparison, or it fails with error 1267 "Illegal mix of collations". Temp tables: see *Staging tables MUST pin the collation* above. (TRUE-82907, jcardinal)
|
|
414
415
|
|
|
415
416
|
## Change history
|
|
416
417
|
|
|
418
|
+
- 2026-10-18 — Core legacy-vs-new table collation gotcha (1267). TRUE-82907 (jcardinal)
|
|
417
419
|
- 2026-10-18 — Core.Records.name required + human naming; record icons in Core.Records (TRUE-82880). (jcardinal)
|
|
418
420
|
- 2026-10-17 — /finish reports central ids used + next free (TRUE-82889). (jcardinal)
|
|
419
421
|
- 2026-10-17 — Second same-folder file also allowed for interceptor rows that must run after the code deploy (TRUE-82889). (jcardinal)
|
|
@@ -6,5 +6,6 @@
|
|
|
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
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. |
|
|
8
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 |
|
|
9
|
+
| [Shell GlobalSearch, NotificationBell, NotificationsInbox (@toga/blox/shell)](features/shell-search-and-notifications.md) | Shared app-shell header search + notifications for every platform app; open when wiring search or the bell into an app, or changing search timing/keys. |
|
|
9
10
|
| [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 |
|
|
10
11
|
| [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,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shell GlobalSearch, NotificationBell, NotificationsInbox (@toga/blox/shell)
|
|
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-18
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-blox/src/shell/search/GlobalSearch.tsx
|
|
13
|
+
- toga25-blox/src/shell/search/searchController.ts
|
|
14
|
+
- toga25-blox/src/shell/search/searchBoxStorage.ts
|
|
15
|
+
- toga25-blox/src/shell/notifications/NotificationBell.tsx
|
|
16
|
+
- toga25-blox/src/shell/notifications/NotificationsInbox.tsx
|
|
17
|
+
related:
|
|
18
|
+
- ../../_underscore/features/global-search.md
|
|
19
|
+
- ../../_underscore/features/notification-engine.md
|
|
20
|
+
- ../architecture.md
|
|
21
|
+
---
|
|
22
|
+
Shared app-shell header search + notifications for every platform app; open when wiring search or the bell into an app, or changing search timing/keys.
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
Every platform app uses these shell pieces — never its own copy. Each app passes a `provider` prop that supplies the data (which clients, which tokens). Backend: [global search](../../_underscore/features/global-search.md), [notification engine](../../_underscore/features/notification-engine.md). Built TRUE-82907.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
**GlobalSearch:**
|
|
31
|
+
- Opens with Ctrl/Cmd-K.
|
|
32
|
+
- `GLOBAL_SEARCH_MIN_CHARS = 3`.
|
|
33
|
+
- **Enter** = tier 1 only; a single exact match jumps straight to the record.
|
|
34
|
+
- **Pause** `GLOBAL_SEARCH_PAUSE_MS = 2000` (same as Vision) = tiers 1 → 2 → 3 chained; results stream in; shows "Searching deeper…".
|
|
35
|
+
- A new keystroke aborts in-flight calls (`AbortController`).
|
|
36
|
+
- `searchBoxUuid` is stable per tenant + user in localStorage — the backend uses it to drop stale searches.
|
|
37
|
+
- Arrow keys + Enter move through results.
|
|
38
|
+
|
|
39
|
+
**NotificationBell / NotificationsInbox:** infinite scroll; counts come from server aggregates, not from loaded rows.
|
|
40
|
+
|
|
41
|
+
**Design:** for shell pieces the design system wins over an app's designer (developer decision 2026-10-18).
|
|
42
|
+
|
|
43
|
+
## Change history
|
|
44
|
+
- 2026-10-18 — Added shell GlobalSearch, NotificationBell, NotificationsInbox. TRUE-82907 (jcardinal)
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
8
|
+
updated: 2026-10-18
|
|
9
9
|
owners: [jcardinal, mhammontree, dfranks, bala, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -317,6 +317,8 @@ WHERE
|
|
|
317
317
|
|
|
318
318
|
### **SQL Naming Conventions**
|
|
319
319
|
|
|
320
|
+
- **American spelling everywhere** — code, table/column names, copy, docs (`color`, not `colour`; `honor`, not `honour`). Team rule 2026-10-18 (jcardinal).
|
|
321
|
+
|
|
320
322
|
#### **Table Names — plural, except the `Logs` cluster**
|
|
321
323
|
|
|
322
324
|
* Table names are **plural**: `Orders`, `Customers`, `PurchaseOrderItems`. This is the default
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-10-
|
|
8
|
+
updated: 2026-10-18
|
|
9
9
|
owners: [jcardinal, apeterson, tcox]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -50,6 +50,8 @@ Anything about env files, build mode, Amplify, the per-environment blox channel,
|
|
|
50
50
|
|
|
51
51
|
## 1. Project structure & module organization — **MUST / SHOULD**
|
|
52
52
|
|
|
53
|
+
- **American spelling** in identifiers, CSS tokens, and copy (`color`, not `colour`) — see [backend-php.md → SQL Naming Conventions](backend-php.md).
|
|
54
|
+
|
|
53
55
|
**MUST: organize feature-first.** A page folder colocates the page's view, its data hook, its
|
|
54
56
|
sub-components, and its CSS. Do not spread one screen's files across parallel `components/`,
|
|
55
57
|
`hooks/`, `styles/` trees by type — group by feature.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
21
21
|
|
|
22
22
|
## 2.0 framework
|
|
23
23
|
|
|
24
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
24
|
+
- **_underscore** (_Underscore) _(framework core)_ — 107 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
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)
|
|
@@ -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) —
|
|
37
|
+
- **toga25-blox** (TOGa Blox 2.5) — 7 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