toga-ai 1.0.1077 → 1.0.1079

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.
@@ -19,3 +19,4 @@
19
19
  | [TOGa 2.0 Client Onboarding Wizard (local tool)](features/toga2-onboarding-wizard.md) | Local browser wizard automating 2.0 client onboarding end-to-end (generates all onboarding SQL + consolidates dbchanges2 loose SQL); local-only, renders secrets |
20
20
  | [TOGa 2.0 User Cross-Client Access SQL Generator](features/toga2-user-cross-client-access-sql.md) | 1.0 App_ script generating SQL to grant an existing 2.0 user cross-client access (Users_Roles grants); open when granting a home-client user into another client |
21
21
  | [URL & Domain Markdown Document Builder](features/url-domain-markdown-document.md) | 1.0 App_ script building a markdown doc of our URLs/domains from the 2.0 API (/environments, /domains); open to regenerate the URL/domain reference. |
22
+ | [Finding and Retiring Unused Elastic Beanstalk Environments (legacy AWS account 502614707982)](workflows/retiring-unused-legacy-elastic-beanstalk-environments.md) | Runbook to prove an EB env in legacy account 502614707982 (us-west-2) is unused (DNS in two accounts + external, shared-ALB rules, CodePipeline) and tear it dow |
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: Finding and Retiring Unused Elastic Beanstalk Environments (legacy AWS account 502614707982)
3
+ framework: "1.0"
4
+ repo: test
5
+ project: Test
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-10-19
10
+ owners: [jcardinal]
11
+ files: []
12
+ related:
13
+ - ../features/create-elastic-beanstalk.md
14
+ - ../../togaview/workflows/route53-cross-account-zone-migration.md
15
+ - ../../../../2.0/apps/worker2/features/alb-target-group-auto-registration.md
16
+ - ../../../../2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md
17
+ ---
18
+
19
+ Runbook to prove an EB env in legacy account 502614707982 (us-west-2) is unused (DNS in two accounts + external, shared-ALB rules, CodePipeline) and tear it down in safe order; also the legacy cost picture, what was retired (TRUE-82821), and open cost follow-ups.
20
+
21
+ ## Summary
22
+
23
+ Use when cutting the legacy AWS bill or cleaning old envs. An env is **used** only if DNS or a queue reaches it — prove that before you terminate anything. Infrastructure only; no app repo changes. `test` is the home for team AWS tooling (see `create-elastic-beanstalk.md`).
24
+
25
+ **Cost picture (Sept 2026, ~$24.4k/mo):** RDS $9.8k, EC2 compute $3.5k + EC2-Other $1.1k, WAF $2.0k (was $558 in July), VPC $1.9k, ELB $1.5k, Bedrock Claude ~$1.2k (≈$3.5k by mid-Oct, rising). No CloudFront in this account.
26
+
27
+ ## Steps
28
+
29
+ 1. **Collect all DNS.** Route 53 lives in BOTH accounts:
30
+ - legacy 502614707982: `agilantsolutions.com`, `truesolutions.com`, `togasuite.com`, `truenetworksolutions.com`
31
+ - prod 654654170868: `togacommerce.com`, `togaiq.com`, `togatech.com`, `togaretail.com`, `togadesk.com`, `togasupply.com`, `togahub.com`, `togatechnology.com`, `togaview.com`
32
+ - **Outside AWS** (`asisystem.com`, `goagilant.com`): ask the developer for a DNS export. Never call an env unused without it.
33
+ 2. **Direct use.** A record points at the env's EB CNAME, its load-balancer DNS, or its single-instance IP → used.
34
+ 3. **Shared-ALB use.** A record points at a shared ALB (today only `true-alb-beta`) → evaluate listener rules **by priority**, with ALB host wildcards (`*` can sit mid-name, e.g. `forecast.*.togatech.com`). The matching rule's target group name = the EB env name (team convention). A default-rule match is not "use" unless intended.
35
+ 4. **Worker-tier envs** have no DNS — judge by their SQS queues.
36
+ 5. **CodePipeline.** Deploy actions name the EB env. All EB actions target removed envs → delete the pipeline. Some still live → remove only the dead action (and any now-empty approval stage).
37
+ 6. **Tear down in this order:**
38
+ 1. delete/edit pipelines
39
+ 2. terminate EB envs
40
+ 3. delete ALB listener rules (a default rule pointing at a removed TG → change to fixed-response 404)
41
+ 4. delete target groups
42
+ 5. delete DNS records
43
+ 6. delete a shared ALB once no rule and no DNS uses it
44
+ 7. **Clean dangling CNAMEs** to `*.elasticbeanstalk.com` names that no longer exist, in every zone incl. external ones.
45
+
46
+ ## Gotchas
47
+
48
+ - **EB terminate leaves shared-ALB pieces behind.** Manually created target groups and listener rules on a shared ALB survive termination — delete them yourself (step 6).
49
+ - **Dangling EB CNAME = subdomain-takeover risk.** Anyone can claim a freed `*.elasticbeanstalk.com` prefix. 4 were found in `asisystem.com` (removed 2026-10).
50
+ - **Claude auto mode blocks destructive AWS deletes** (`codepipeline delete-pipeline`, `elbv2 delete-load-balancer`) even with `Bash(*)`. Allow rules in `settings.local.json` (`Bash(aws codepipeline:*)`, `Bash(aws elasticbeanstalk:*)`, `Bash(aws elbv2:*)`, `Bash(aws route53:*)`) unblock most; `delete-load-balancer` stays blocked — give that command to the developer. The `start-gate` hook blocks Claude from editing `settings*.json`; the developer edits it.
51
+ - **Externally referenced envs look dead from AWS.** `asi-api-prod` (`api.asisystem.com`) and `agilant-vision-prod` (`vision.asisystem.com`) have no Route 53 record but are live.
52
+
53
+ ## Retired 2026-10-15 → 10-19 (TRUE-82821, approved by jcardinal)
54
+
55
+ - **EB envs (24):** agilant-forecast-alpha, agilant-goagilant-alpha, agilant-store-alpha, agilant-store-beta, true-togacommerce-alpha/-beta/-gamma, agilant-vision-alpha, agilant-fujitsu-prod, agilant-goagilant-prod, cloudsupporthq-togadesk-prod, true-toga-commmerce-beta, true-togadesk-beta, true-togadesk-production, true-togahub-production, true-vision-alpha, agilant-api-alpha, agilant-forecast-beta, agilant-forecast-prod, agilant-toga-alpha, agilant-togadesk-alpha, agilant-togaview-alpha, agilant-worker-alpha, true-api-beta.
56
+ - **CodePipelines:** 38 deleted (18 already pointed at missing envs); `ITAD-Production` and `TOGaDesk-Production` edited to drop dead actions; 29 remain.
57
+ - **Shared ALBs:** `true-alb-alpha`, `true-alb-gamma` deleted. `true-alb-beta` remains (default rule → `agilant-vision-beta`).
58
+ - **DNS:** tools.beta / vision.alpha / tools.alpha / api-alpha / api.alpha `agilantsolutions.com`; `forecast.togatech.com`, `forecast.beta.togatech.com`; `cxml.beta.togahub.com`, `api.client.togahub.com`; all `*.alpha.togaview.com`, `*.alpha.togadesk.com`; `toga.alpha` / `togadesk.alpha.togatech.com`.
59
+ - **RDS Aurora (no final snapshot):** alpha1-cluster, sprint1-cluster, ui1-cluster, doa-client-writer. Remaining: beta1, demo1, gamma1, gamma, hotfix1, prod, production-logs, stage1.
60
+ - **Kept:** worker envs `agilant-worker`, `agilant-worker-beta`; `asi-api-prod`, `agilant-vision-prod`.
61
+
62
+ ## Open follow-ups
63
+
64
+ 1. WAF cost ~4x since July — not investigated.
65
+ 2. Bedrock Claude spend rising month over month — find the caller.
66
+ 3. Review the 8 remaining RDS clusters (largest cost line).
67
+ 4. No AWS Budgets / Cost Anomaly alerts set up on the legacy account.
68
+
69
+ ## Change history
70
+ - 2026-10-19 — Created from TRUE-82821 legacy cost cleanup: unused-env checks, teardown order, retired list, open follow-ups (jcardinal)
@@ -38,6 +38,7 @@
38
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. |
39
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 |
40
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 |
41
+ | [Location, Unit (asset), Ticket Department — notes + files tables, LocationTypes icons](features/location-unit-department-notes-files.md) | Shared Client-DB notes/files child tables for Locations, Units (assets) and TicketDepartments, plus LocationTypes icon columns; open before adding notes/files t |
41
42
  | [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 |
42
43
  | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` has `__get` but NO `__isset`, so `isset()`/`??`/`empty()` on a magic DB field are unreliable or false; open before probing an optional model field or d |
43
44
  | [_Model::save() parent FK cascade — stored-SQL-field recompute deadlocks](features/model-save-parent-cascade-stored-field-deadlock.md) | Why a child insert deadlocks (MySQL 1213): `_Model::save()` re-saves the parent to recompute stored SQL fields; open when a child-insert path deadlocks or befor |
@@ -61,6 +62,7 @@
61
62
  | [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | How the SO modal "PO Number" field is sourced (one surface element → `_purchaseOrders` calc field → tenant SQL); open before touching PO-number display, the ups |
62
63
  | [SO↔PO and TO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | The two SO↔PO (and two TO↔PO) bridge tables mean opposite things (upstream = customer PO, downstream = vendor PO); open when an SO/TO↔PO query returns an empty |
63
64
  | [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | Per-client status-filter dropdown on the sales-orders table, driven by a Surface `FILTER_SET`; open when the offered statuses are wrong for a tenant or when onb |
65
+ | [Service request links (ServiceRequests_Tickets, ServiceRequests_ServiceRequests bridges)](features/service-request-links.md) | Many-to-many links from a service request to tickets and to other service requests; open before linking SRs, reading `_linkCount`, or touching the old `ServiceR |
64
66
  | [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
65
67
  | [Ticket SLA engine (_Component_Sla) — two clocks, per-stage effects, client switch + operator recompute](features/sla-engine.md) | How 2.0 ticket SLA due times, pauses and the per-client switch are computed and written (`_Component_Sla`); open when an SLA countdown is wrong, a stage does no |
66
68
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
@@ -69,6 +71,7 @@
69
71
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | Runtime for the platform Surface UI layer (`_Model_Core_Surface::resolve`/`metaGroup`, replaces `Page::meta`); open for surface resolution, the override cascade |
70
72
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | How a 2.0 table-view column renders its value as a clickable link (`hyperlinkRecordFieldId` → ACL field-read → computed URL → render); open when a configured hy |
71
73
  | [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | How `TableViewJoins` rows become a view's join SQL — aliasing, chained multi-hop joins, ACL gating, and the three ways a view breaks; open before adding/rebasin |
74
+ | [Table view presets (saved views — TableViewPresets.config whitelist)](features/tableview-presets.md) | A user's saved table view stored as one validated JSON `config`; open before adding anything new to what a saved view remembers. |
72
75
  | [2.0 Ticket status (`_status`) is derived from the stage's linked TicketStatuses row](features/ticket-status-from-stage.md) | How a 2.0 ticket's `_status` is computed (stage → TicketStatuses link, else `in-progress` / `new-ticket`); open when a ticket shows the wrong status or a status |
73
76
  | [Talos Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | The 2.0 client for the Talos AI gateway (`_Component_Api_Togaiq`); open to call an LLM (generate/translate) from a 2.0 model/interceptor, or to debug "translati |
74
77
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | How tracking numbers moved from scalar FK columns to `*_TrackingNumbers` bridge tables across ASN/IF/IR; open before writing/reading tracking, choosing which br |
@@ -64,7 +64,7 @@ Added 2026-10-17 (TRUE-82871, built for TOGa Desk, usable by any app). Copies th
64
64
  could delete a Files row a contact still uses.
65
65
  - **Client schemas drift** (e.g. `Client_True.TransferOrders` has no `contactId`) — the delete map probes optional
66
66
  columns with `columnExists()`.
67
- - **TableViewPresets are per-user** (`userId` NOT NULL) — "default preset views" from a design cannot be seeded.
67
+ - **Saved views cannot be seeded** — see [table view presets](./tableview-presets.md).
68
68
 
69
69
  ## Change history
70
70
  - 2026-10-17 — Created: ContactTypes.slug + seeds, Contacts_Files, ContactNotes, contact activity root, contact delete (TRUE-82871). (jcardinal)
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Location, Unit (asset), Ticket Department — notes + files tables, LocationTypes icons
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-19
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/LocationNote.php
13
+ - _underscore/Model/Client/Locations/File.php
14
+ - _underscore/Model/Client/UnitInternalNote.php
15
+ - _underscore/Model/Client/Units/File.php
16
+ - _underscore/Model/Client/TicketDepartmentNote.php
17
+ - _underscore/Model/Client/TicketDepartments/File.php
18
+ - dbchanges2/Client/2026-10-18l - TRUE-82911 LocationUnitNotesFiles.sql
19
+ - dbchanges2/Client/2026-10-19a - TRUE-82911 DepartmentNotesFiles.sql
20
+ - dbchanges2/Client/2026-10-19b - TRUE-82911 LocationTypeIcons.sql
21
+ related:
22
+ - ./customer-locations-files-notes.md
23
+ - ./contact-notes-files-types.md
24
+ - ./note-record-authorship.md
25
+ - ./record-hard-delete.md
26
+ - ./record-activity-timeline.md
27
+ ---
28
+ Shared Client-DB notes/files child tables for Locations, Units (assets) and TicketDepartments, plus LocationTypes icon columns; open before adding notes/files to one of these records or touching `UnitNotes`.
29
+
30
+ ## Summary
31
+
32
+ Added 2026-10-18/19 (TRUE-82911, built for TOGa Desk, usable by any app). Same shape as the Customer/Contact
33
+ pattern ([customer related records](./customer-locations-files-notes.md)) — read that doc for the shared shape.
34
+
35
+ | Table | Shape | Core Records |
36
+ |---|---|---|
37
+ | `LocationNotes` | write-once note, server-stamped `authorUserId` | 414 `location-notes` |
38
+ | `Locations_Files` | file link (create / read / delete, no update) | 415 `location-files` |
39
+ | `Units_Files` | file link | 416 `unit-files` |
40
+ | `UnitInternalNotes` | write-once note, server-stamped author | 417 `unit-internal-notes` |
41
+ | `TicketDepartmentNotes` | write-once note, server-stamped author | 420 `ticket-department-notes` |
42
+ | `TicketDepartments_Files` | file link | 421 `ticket-department-files` |
43
+ | `LocationTypes.iconToken` / `iconWeightToken` | nullable `VARCHAR(64)` / `VARCHAR(32)` | RecordFields 3104-3105 |
44
+
45
+ RecordFields 3062-3105 cover all of the above. SQL: `Client/2026-10-18l`, `2026-10-19a`, `2026-10-19b` (+ Core
46
+ `2026-10-18s`, `2026-10-19c`, `2026-10-19d`).
47
+
48
+ ## How it works
49
+
50
+ - **Notes:** author stamped in `save()` from `_Model::getRequestUserId()`; no client user → 400; updates refused
51
+ ([note authorship](./note-record-authorship.md)).
52
+ - **Files:** create / read / delete only. Each `*_Files` table is in `_Component_RecordDelete::FILE_REFERENCE_TABLES`.
53
+ - **Delete:** Locations (Records 11), Units (31), TicketDepartments (106) purge their notes + file links on a normal
54
+ table `DELETE` ([record hard delete](./record-hard-delete.md)).
55
+ - **Activity:** roots `locations` → `location-activity`, `ticket-departments` → `department-activity`; the three
56
+ note routes are in `CREATE_HIDDEN_ROUTES` ([record activity timeline](./record-activity-timeline.md)).
57
+ - **LocationTypes icons:** null = front end shows a pin. Production seed sets Shipping = `truck`,
58
+ Warehouse = `warehouse` only where null.
59
+
60
+ ## Gotchas
61
+
62
+ - **Asset (Unit) notes from a user go in `UnitInternalNotes`, never `UnitNotes`.** `UnitNotes` holds typed condition
63
+ notes: toga2-supply reads them by position (`unitNotes[0]`, `[1]`) and the BofA import writes them with no
64
+ signed-in user, so an author-stamped `save()` there would break that import (cto agreed, 2026-10-18).
65
+
66
+ ## Change history
67
+ - 2026-10-19 — Created: Location / Unit / TicketDepartment notes + files, LocationTypes icon columns (TRUE-82911). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-19
10
10
  owners: [apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/SalesOrderNote.php
@@ -64,10 +64,12 @@ Before building anything that shows or filters by note author:
64
64
 
65
65
  - **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
66
66
  - **A notes list UI has no author to show.** Design for it, or the field silently renders blank for every row.
67
- - **Exception — server-stamped author:** `CaseNotes`, `CustomerNotes`, `ContactNotes`, `IssuerNotes`, `CertificationNotes`, `AttributeNotes`, `UserNotes` carry `authorUserId`, stamped in `::save()` from `_Model::getRequestUserId()` (write-once). Copy that pattern for the real fix. The `save()` body is now copied 7 times — **consolidation debt:** move it to a shared trait/base before adding another. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md), [Contact related records](./contact-notes-files-types.md).
67
+ - **Exception — server-stamped author:** `CaseNotes`, `CustomerNotes`, `ContactNotes`, `IssuerNotes`, `CertificationNotes`, `AttributeNotes`, `UserNotes`, `LocationNotes`, `UnitInternalNotes`, `TicketDepartmentNotes` carry `authorUserId`, stamped in `::save()` from `_Model::getRequestUserId()` (write-once). Copy that pattern for the real fix. The `save()` body is now copied 10 times — **consolidation debt:** move it to a shared trait/base before adding another. See [Case status + stage gate](./case-status-and-stage-gate.md), [Customer related records](./customer-locations-files-notes.md), [Contact related records](./contact-notes-files-types.md), [Location / unit / department notes](./location-unit-department-notes-files.md).
68
+ - **Do not add author stamping to `UnitNotes`** — the BofA import writes it with no signed-in user and toga2-supply reads it by position. User-written asset notes use `UnitInternalNotes` ([why](./location-unit-department-notes-files.md#gotchas)).
68
69
  - **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
69
70
 
70
71
  ## Change history
72
+ - 2026-10-19 — LocationNotes, UnitInternalNotes, TicketDepartmentNotes added (10 copies of save()); UnitNotes stays author-less (TRUE-82911). (jcardinal)
71
73
  - 2026-10-17 — UserNotes added; seven author-stamped note models now copy one save() body (TRUE-82872). (jcardinal)
72
74
  - 2026-10-17 — ContactNotes added as third server-stamped-author note model; save() body is a consolidation candidate (TRUE-82871). (jcardinal)
73
75
  - 2026-10-17 — CustomerNotes added as a second server-stamped-author note model (TRUE-82870). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-19
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Component/RecordActivity/RecordActivity.php
@@ -33,7 +33,7 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
33
33
  - **Roots** = `ROOT_ACTIVITY_SURFACE_SLUGS` (route → activity Surface slug): `tickets` →
34
34
  `ticket-activity`, `units` → `asset-activity`, `cases` → `case-activity`, `service-requests` →
35
35
  `service-request-activity`, `customers` → `customer-activity`, `contacts` → `contact-activity`,
36
- `issuers`, `certifications`, `attributes`, `users` → `<singular>-activity`. Any other route → 400. **A new root = one map entry + its Surface
36
+ `locations` → `location-activity`, `ticket-departments` → `department-activity`, `issuers`, `certifications`, `attributes`, `users` → `<singular>-activity`. Any other route → 400. **A new root = one map entry + its Surface
37
37
  (`dbchanges2` `Core/`).** Engine (`Source` reads Logs, `Reader` checks as the reader through api2)
38
38
  is generic; no new client ACL needed for a new root.
39
39
  - **Children:** `Logs.RecordParent`, level by level, max `Source::MAX_DEPTH` (3) levels and
@@ -47,7 +47,8 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
47
47
  4. FK values → `{uuid, label[, slug]}`, `{missing:true}`, or `{value:null}` — never a raw id.
48
48
  - **CREATE rows of `CREATE_HIDDEN_ROUTES`** (`ticket-notes`, `ticket-note-files`, `unit-notes`,
49
49
  `case-notes`, `service-request-notes`, `customer-notes`, `contact-notes`, `issuer-notes`,
50
- `certification-notes`, `attribute-notes`, `user-notes`) are dropped — the host screen merges the notes feed.
50
+ `certification-notes`, `attribute-notes`, `user-notes`, `location-notes`, `unit-internal-notes`,
51
+ `ticket-department-notes`) are dropped — the host screen merges the notes feed.
51
52
  Note edits/deletes still show.
52
53
  - **Paging:** cursor = `Logs.Record` id; `nextCursor` null at the end. Scan bounded
53
54
  (`MAX_SCAN_BATCHES` × `SCAN_BATCH_SIZE`); a page cut by the bound still returns a `nextCursor`.
@@ -74,6 +75,7 @@ client `AclRecordScripts`. Data source: [record change audit log](record-change-
74
75
  `LOG_REDACTED_FIELD_NAMES`) and on read (`isRedactedLogField`).
75
76
 
76
77
  ## Change history
78
+ - 2026-10-19 — Added roots `locations`, `ticket-departments`; three new note routes hidden (TRUE-82911). (jcardinal)
77
79
  - 2026-10-17 — Added root `users` + `user-notes`; listed `issuers` / `certifications` / `attributes` roots already in code (TRUE-82872). (jcardinal)
78
80
  - 2026-10-17 — Added root `contacts` + `contact-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82871). (jcardinal)
79
81
  - 2026-10-17 — Added root `customers` + `customer-notes` to `CREATE_HIDDEN_ROUTES` (TRUE-82870). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-18
9
+ updated: 2026-10-19
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Component/RecordDelete/RecordDelete.php
@@ -15,9 +15,11 @@ files:
15
15
  - _underscore/Model/Client/Ticket.php
16
16
  - _underscore/Model/Client/Case.php
17
17
  - _underscore/Model/Client/ServiceRequest.php
18
+ - _underscore/Model/Client/Phase.php
18
19
  related:
19
20
  - ../../api2/features/api-payload-interceptors.md
20
21
  - ./customer-locations-files-notes.md
22
+ - ./location-unit-department-notes-files.md
21
23
  - ./record-activity-timeline.md
22
24
  ---
23
25
  Map-driven hard delete of everything hanging off one record (block / detach / delete children / orphan Files); open before making a record deletable or adding a table with a Files FK.
@@ -27,7 +29,9 @@ Map-driven hard delete of everything hanging off one record (block / detach / de
27
29
  `DELETE /v2/<route>/{uuid}` on an opted-in record runs a PRE DELETE interceptor that calls
28
30
  `_Component_RecordDelete::purge(['model' => static::class, 'id' => $id])`. `purge()` cleans the related
29
31
  rows from a map the model declares in `relatedRows()`; api2 then deletes the record row itself. Used by
30
- Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the full contract — read it before editing.
32
+ Tickets, Cases, ServiceRequests, Customers, Contacts, Roles, Attributes, Locations, Units, TicketDepartments, Phases.
33
+ The class DocBlock is the full contract — read it before editing. **Make a record deletable through the normal table
34
+ `DELETE` + this interceptor — never a scripted API** (developer rule, 2026-10-19).
31
35
 
32
36
  ## How it works
33
37
 
@@ -53,6 +57,13 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
53
57
  ContactNotes, ContactAttempts, ContactMessagingOptIns, Campaigns_Contacts, Campaigns_MeetWithContacts,
54
58
  Contacts_Tags, Contacts_Vendors, Contacts_Locations, Locations_Contacts, ContactAddresses / EmailAddresses /
55
59
  PhoneNumbers. `preDelete` clears the contact's `primary*` pointers first (they and the children point at each other).
60
+ - **Location (Records 11) / Unit (31) / TicketDepartment (106) rule (`Core/2026-10-19a`, `2026-10-19e`):** delete
61
+ their notes (`LocationNotes`, `UnitInternalNotes`, `TicketDepartmentNotes`) and file links (`Locations_Files`,
62
+ `Units_Files`, `TicketDepartments_Files`, + orphan Files). Every other link (tickets etc.) is left to the DB FK —
63
+ see gotcha. Tables: [location / unit / department notes + files](./location-unit-department-notes-files.md).
64
+ - **Phase / stage rule (Records 359, `Core/2026-10-18q`):** blocked while any `Scopes.phaseId` uses it (no FK, so
65
+ the blocker is the only guard). `preDelete` returns a 400 naming the count + first 3 scopes; `purge()` re-checks
66
+ under the row lock.
56
67
  - **Role rule (Records 274):** blocked by Users_Roles / Apis_Roles; ACL rows handled after `purge()` — full rule in
57
68
  [ACL Capabilities § Role delete](acl-capabilities.md).
58
69
 
@@ -65,6 +76,8 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
65
76
 
66
77
  - **New table with a `fileId` FK → add it to `FILE_REFERENCE_TABLES`.** Missing it makes the Files DELETE fail on
67
78
  the FK and roll back every delete that touches a shared file (loud, not silent).
79
+ - **A link not in the map is refused by its DB FK — as a 500, not a friendly 400.** Fine as a safety net; add a
80
+ `blockers` entry when the user needs a readable reason (Locations / Units / TicketDepartments still rely on FKs).
68
81
  - **Client override of `relatedRows()` must merge `parent::relatedRows()` key by key;** a client `preDelete`
69
82
  replaces the shared one and must call `parent::preDelete()`.
70
83
  - **Use `$api->recordUuid ?? null`, never `isset($api->recordUuid)`** — `_Component` has `__get` but no `__isset`.
@@ -73,6 +86,7 @@ Tickets, Cases, ServiceRequests, Customers, Contacts. The class DocBlock is the
73
86
  losing the link would lose meaning (orders, required FKs like `Contacts.customerId`).
74
87
 
75
88
  ## Change history
89
+ - 2026-10-19 — Locations, Units, TicketDepartments, Phases opted in; three `*_Files` tables added to FILE_REFERENCE_TABLES; table DELETE, never scripted API (TRUE-82911). (jcardinal)
76
90
  - 2026-10-18 — Roles opted in (TRUE-82880). (jcardinal)
77
91
  - 2026-10-17 — Contacts opted in (block / detach / cascade map, schema-drift probes); Contacts_Files added to FILE_REFERENCE_TABLES (TRUE-82871). (jcardinal)
78
92
  - 2026-10-17 — Created; Customers opted in, Customers_Files added to FILE_REFERENCE_TABLES (TRUE-82870). (jcardinal)
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Service request links (ServiceRequests_Tickets, ServiceRequests_ServiceRequests bridges)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-19
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/ServiceRequests/Ticket.php
13
+ - _underscore/Model/Client/ServiceRequests/ServiceRequest.php
14
+ - _underscore/Model/Client/ServiceRequest.php
15
+ - _underscore/Component/TicketMerge/TicketMerge.php
16
+ - dbchanges2/Client/2026-10-18m - TRUE-82911 ServiceRequestLinks.sql
17
+ - dbchanges2/Core/2026-10-18u - TRUE-82911 ServiceRequestLinksCore.sql
18
+ related:
19
+ - ./record-hard-delete.md
20
+ ---
21
+ Many-to-many links from a service request to tickets and to other service requests; open before linking SRs, reading `_linkCount`, or touching the old `ServiceRequests.ticketId` / `Tickets.serviceRequestId` FKs.
22
+
23
+ ## Summary
24
+
25
+ The single FKs `ServiceRequests.ticketId` and `Tickets.serviceRequestId` hold one link each, so many links per kind
26
+ moved to two bridge tables (TRUE-82911; toga25-desk ADR 0165 amends 0119/0127). Core Records 418
27
+ `service-request-tickets`, 419 `service-request-service-requests`.
28
+
29
+ ## How it works
30
+
31
+ - **`ServiceRequests_Tickets`**, **`ServiceRequests_ServiceRequests`** — link rows are added or removed, never edited
32
+ (PUT/PATCH → 400).
33
+ - **SR ↔ SR stored once, lower id first.** Query both columns when reading a request's links.
34
+ - **PRE POST checks:** caller needs READ on both records; refuses self-links, duplicates, and pairs already tied by
35
+ the old FKs (400 with a readable message).
36
+ - **Old FK links still count as links.** `ServiceRequest::_linkCount` (non-stored) counts both bridges plus
37
+ `Cases_ServiceRequests`; unlinking an FK-tied pair clears the FK instead of deleting a bridge row.
38
+ - **Delete:** the service request's `relatedRows()` clears its link rows.
39
+ - **Ticket merge:** `_Component_TicketMerge` moves `ServiceRequests_Tickets` rows to the surviving ticket and drops
40
+ duplicates.
41
+
42
+ ## Change history
43
+ - 2026-10-19 — Created: SR ↔ ticket and SR ↔ SR bridges replace one-link FKs (TRUE-82911). (jcardinal)
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Table view presets (saved views — TableViewPresets.config whitelist)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-19
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/TableViewPreset.php
13
+ related:
14
+ - ./tableview-joins.md
15
+ - ../../api2/features/api-payload-interceptors.md
16
+ ---
17
+ A user's saved table view stored as one validated JSON `config`; open before adding anything new to what a saved view remembers.
18
+
19
+ ## Summary
20
+
21
+ `/v2/table-view-presets` (Core Records 352, generic dispatcher, no controller). One row = one user's named snapshot
22
+ of a table: columns, up to 3 sorts, filters, group, search. Per-user (`userId` NOT NULL, own rows only) — a
23
+ "default view" from a design cannot be seeded for everyone.
24
+
25
+ ## How it works
26
+
27
+ - **PRE POST / PRE PUT** stamp ownership and run `validateConfig()`. POST needs `config`; a PUT without `config`
28
+ (rename, set default) keeps the stored one.
29
+ - **`validateConfig()` rebuilds the JSON from a whitelist:** `columns`, `sorts`, `filters`, `group`, `search`.
30
+ Field tokens must match `FIELD_TOKEN_PATTERN`; filter operators only `eq`, `in`, `ge`, `lt`, `contains`; values
31
+ scalar only. Bad input → 400.
32
+ - **`search`** (added 2026-10-19): optional string, trimmed, max `MAX_SEARCH_LENGTH` 255, blank → `null`; not a
33
+ string or too long → 400. Old presets without it stay valid. Stored only, never used in SQL here.
34
+
35
+ ## Gotchas
36
+
37
+ - **A new config key is silently dropped on save** unless you add it to the whitelist in `validateConfig()` with
38
+ its own validator. No error — the key just never comes back.
39
+
40
+ ## Change history
41
+ - 2026-10-19 — Created; `search` added to the config whitelist (TRUE-82911). (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-19
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - api2/Component/Api/CrossClient/CrossClient.php
@@ -132,6 +132,11 @@ Page 1 of 25 still fetches exactly 25; page 254 fetches 500 per pass — ~13 rou
132
132
  `client=` on another tenant's token makes `resolveScope()` fan out to THAT tenant's `Users_Clients`
133
133
  (cross-tenant read). Seeded uuids also repeat across `Client_*` DBs, so multi-client UI rows must be
134
134
  keyed by client uuid + record uuid. Full rule: toga25-desk ADR 0154, `GOTCHAS.md`.
135
+ - **⚠ One ungranted field fails the WHOLE cross-client read with 403** — "Cross-client sub-request failed … You do
136
+ not have the necessary permissions to read the specified fields". This includes `_`-prefixed calc fields, which a
137
+ single-client read lets through. Example: Items `_label` (RecordFields 2599) had no `AclFieldPermissions` grant
138
+ anywhere until 2026-10-19 (`Client/2026-10-19e`, `Core/2026-10-19g`; granted like `_imageUrl` 1276: Core Super
139
+ User + client capability Base). Before a consumer names a field, check the grant exists for the caller's roles.
135
140
  - **`curl_multi` busy-spin guard** — both multi loops `usleep(100)` when `curl_multi_select() === -1`.
136
141
  - **Known limitation — deep page-number paging is inherently O(page).** Serving page 4430 means materializing all 110,747 preceding rows into the cache. Adaptive chunking makes that cheaper, not cheap: cost still grows with depth, because a page **number** cannot be resolved without knowing everything before it. The durable answer is **exposing keyset cursors to the caller** so cost is independent of depth — the mechanism already exists (V2 supports `seekValues` + `seekUuid` correctly). Raised with the developer 2026-07-28 and **deliberately left unbuilt / not yet scoped**: a known limitation and candidate direction, not pending work.
137
142
 
@@ -145,6 +150,7 @@ Page 1 of 25 still fetches exactly 25; page 254 fetches 500 per pass — ~13 rou
145
150
  - [_Model magic-field access](../../_underscore/features/model-magic-field-access.md)
146
151
 
147
152
  ## Change history
153
+ - 2026-10-19 — One ungranted field (calc fields too) fails the whole cross-client read with 403; Items `_label` granted (TRUE-82911). (jcardinal)
148
154
  - 2026-10-17 — Home vs linked user rows; `Users_Clients` is the reach list, linked rows drift (TRUE-82872). (jcardinal)
149
155
  - 2026-10-17 — Fetch scope is `client=` OR own token, never both; key multi-client rows by client + record uuid (TRUE-82903). (jcardinal)
150
156
  - 2026-10-17 — Internal-request writes must add their own `cacheInvalidations` (TRUE-82889). (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-18
9
+ updated: 2026-10-19
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -389,7 +389,9 @@ where=(field:op:value,LOGIC,field:op:value,(nested,OR,nested))
389
389
  `field:ne:null` → `IS NOT NULL`.
390
390
  - **A dotted FK path (`case.uuid`) in `where` fails `EV-8` on a list GET.** Filter by a parent FK
391
391
  through a join alias: `ojoin=Cases:Cases.id=Tickets.caseId&where=(Cases.uuid:eq:<uuid>)` (or a
392
- `TableViewJoins` row that gives the alias). A path through an **inherent child**
392
+ `TableViewJoins` row that gives the alias). **Self-join (FK to the same table, e.g.
393
+ `parentLocation.uuid`) needs an alias:** `ojoin=Locations@p:p.id=Locations.parentLocationId&where=(p.uuid:eq:<uuid>)`.
394
+ A path through an **inherent child**
393
395
  (`ticketUsers.user.uuid`) does work — but only when the **first** segment is an inherent child
394
396
  of the route (`buildChildPathWhereSql`). A path that starts with a forward FK hop fails
395
397
  (`service-requests` → `ticket.ticketUsers.user.uuid`). No-api2-change fix: a TableView that
@@ -568,6 +570,10 @@ ACL gate for calculated fields lives at **:7216** in `getFullModelData()` — wh
568
570
  **Corollary for the `fields=`-forces-`EZ-2` diagnostic above:** re-requesting with an explicit
569
571
  `fields=` list to smoke out a denied field **only works on plain columns**. It will never produce an
570
572
  `EZ-2` for a `_`-prefixed field, so a silent `0` is not evidence that the grant exists.
573
+
574
+ **⚠ The bypass does NOT hold on a cross-client (`client=`) read** — an ungranted calc field there fails the whole
575
+ read with 403. Grant every `_` field a consumer names. See
576
+ [cross-client gotchas](cross-client-data-retrieval.md#gotchas).
571
577
  - **`EZ-1` vs `EZ-2` for a consumer:** `EZ-1` = record-level block (`data.<route>` is `null`) —
572
578
  hard blocked, nothing usable. `EZ-2` = field-level — on a **LIST** it is
573
579
  *degraded-but-usable*; on a **by-uuid READ** it is **fatal**.
@@ -650,6 +656,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
650
656
  [V2 request logging](request-logging.md).
651
657
 
652
658
  ## Change history
659
+ - 2026-10-19 — Self-join `where` needs an `ojoin` alias (`Locations@p`); `_` field ACL bypass does not apply cross-client (TRUE-82911). (jcardinal)
653
660
  - 2026-10-18 — Grouped queries: stored root group key now selected + ACL-gated; known gaps (joined key, total count, `ORDER BY uuid` under ONLY_FULL_GROUP_BY) (TRUE-82876). (jcardinal)
654
661
  - 2026-10-17 — Child-path `where` must start at an inherent child; FK-first path → TableView join alias (TRUE-82872). (jcardinal)
655
662
  - 2026-10-17 — Multi-sort comma list + `uuid` tie-breaker for stable paging; sort on calc works; refreshed where-path line refs. (jcardinal)
@@ -12,7 +12,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
12
12
  - **togaview** (TOGa View) — 8 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
13
13
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
14
14
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
15
- - **test** (Test) — 17 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
15
+ - **test** (Test) — 18 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 4 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
17
  - **store** (Store) — 4 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
18
18
  - **tools** (Tools) — 25 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
@@ -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)_ — 107 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
24
+ - **_underscore** (_Underscore) _(framework core)_ — 110 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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1077",
3
+ "version": "1.0.1079",
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",