toga-ai 1.0.509 → 1.0.511
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/1.0/apps/tools/INDEX.md +2 -1
- package/knowledge/1.0/apps/tools/architecture.md +24 -2
- package/knowledge/1.0/apps/tools/features/clickup-sprint-dashboard.md +13 -2
- package/knowledge/1.0/apps/tools/features/design-demo-admin.md +1 -2
- package/knowledge/1.0/apps/tools/features/legacy-email-notifier.md +15 -2
- package/knowledge/1.0/apps/tools/features/talos-pricing-ui.md +20 -0
- package/knowledge/1.0/apps/tools/features/theme-light-dark.md +209 -0
- package/knowledge/1.0/standards/frontend.md +42 -3
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +64 -1
- package/knowledge/2.0/apps/api2/features/request-logging.md +31 -1
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/INDEX.md +2 -1
- package/knowledge/clients/compass-usa/workflows/odp-edi-import-recovery.md +111 -0
- package/knowledge/clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md +70 -9
- package/knowledge/clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md +44 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
-
| [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md |
|
|
5
|
+
| [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md, tools/assets/css/theme.css, tools/assets/js/theme.js |
|
|
6
6
|
| [ClickUp Sprint Dashboard (tools /clickup/react)](features/clickup-sprint-dashboard.md) | A native **ClickUp sprint dashboard** in the 1.0 `tools` app at route `/clickup/react`, built for a wall-mounted **TV display** ("TOGa IQ" internal sprint metri | tools/_/app/clickup/sprint.php, tools/v2/sprints/tile/index.php, tools/v2/sprints/current/index.php, tools/v2/sprints/status-breakdown/index.php, tools/v2/sprints/worktype-breakdown/index.php, tools/v2/sprints/points-by-dev/index.php, tools/v2/sprints/burndown/index.php, tools/assets/clickup/sprint-dashboard.html, tools/mvc/clickup/react/get.php, tools/mvc/login/get.php, tools/_/app/nav.php |
|
|
7
7
|
| [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
|
|
8
8
|
| [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
|
|
@@ -14,4 +14,5 @@
|
|
|
14
14
|
| [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
|
|
15
15
|
| [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | > **PER-AI-MODEL, DATA-DRIVEN SCOPING (2026-07-29).** The KB-documents and Vocabulary admin > UIs were refactored from a single hard-coded **"development-team"* | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/knowledge-bases/get.php, tools/mvc/talos/knowledge-bases/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/talos/bedrock.php, tools/_/app/pg.php, tools/_/app/model/true/aimodels.php, tools/_/app/model/true/vectorindexes.php, tools/_/app/model/true/aimodels_vectorindexes.php, tools/_/app/worker.php, tools/_/app/nav.php, tools/config.production.ini, tools/config.alpha.ini, tools/.platform/httpd/conf.d/timeouts.conf, tools/.platform/hooks/prebuild/01-install-php-pgsql.sh, tools/.platform/hooks/postdeploy/01-restart-php.sh |
|
|
16
16
|
| [Talos Pricing UI (Contracts, Pricing Dashboard, Usage, Settings + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus the estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/_/app/talos/usage.php, tools/mvc/talos/contracts/get.php, tools/mvc/talos/contracts/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/usage/get.php, tools/mvc/talos/settings/get.php, tools/mvc/talos/settings/post.php, tools/assets/css/style.css |
|
|
17
|
+
| [App-Wide Colour Theme (light / dark / auto)](features/theme-light-dark.md) | A **light / dark / auto** colour theme for the *entire* Tools app, built as a single semantic-token layer (`assets/css/theme.css`) rather than per-page edits. | tools/assets/css/theme.css, tools/assets/js/theme.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/common/header.php, tools/mvc/login/get.php, tools/assets/css/style.css, tools/assets/css/design.css, tools/assets/css/login.css, tools/assets/css/cloudfront-clients.css, tools/assets/css/gchart.css, tools/mvc/talos/usage/get.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/contracts/get.php, tools/mvc/clickup/dashboard/get.php, tools/mvc/email-migration/notify/post.php |
|
|
17
18
|
| [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json, tools/ebs/setup_export_cache_folders.php, tools/_/app/frameworkindex.php |
|
|
@@ -6,7 +6,7 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/index.php
|
|
@@ -23,7 +23,10 @@ files:
|
|
|
23
23
|
- tools/mvc/get.php
|
|
24
24
|
- tools/mvc/_TEMPLATE/get.php
|
|
25
25
|
- tools/docs/ADDING_A_TOOL.md
|
|
26
|
+
- tools/assets/css/theme.css
|
|
27
|
+
- tools/assets/js/theme.js
|
|
26
28
|
related:
|
|
29
|
+
- ./features/theme-light-dark.md
|
|
27
30
|
- ./features/saml-sso-auth.md
|
|
28
31
|
- ./features/persona-gated-navigation.md
|
|
29
32
|
- ./features/developer-tools.md
|
|
@@ -47,7 +50,13 @@ double-gated (`[internal] dev_mode` AND `App_Registry::inDevMode()`). `config.*.
|
|
|
47
50
|
(incl. `config.prod.ini`) is committed with plaintext production secrets that were **publicly
|
|
48
51
|
web-readable** until the front-controller deny blocks added 2026-07-24 (see Known issues);
|
|
49
52
|
rotation of every exposed secret is still owed. Never add more secrets, and keep config files
|
|
50
|
-
denied at the front controller / out of the web root.
|
|
53
|
+
denied at the front controller / out of the web root. New CSS must use `--c-*` tokens from
|
|
54
|
+
`assets/css/theme.css`, never raw colour literals. **`assets/css/style.scss` is DEAD — edit
|
|
55
|
+
`style.css` directly** (the `.css` has drifted ~450 lines ahead; there is no sass binary or
|
|
56
|
+
`package.json`, so recompiling the SCSS would silently revert shipped work).
|
|
57
|
+
`App_FrameworkIndex::setHeaderCode()` is the **only** hook for markup in `<head>`, and it
|
|
58
|
+
**SETS rather than appends** — the theme bootstrap is its sole caller, so a second call
|
|
59
|
+
silently clobbers it.
|
|
51
60
|
|
|
52
61
|
## Boot & structure
|
|
53
62
|
|
|
@@ -72,6 +81,14 @@ sibling apps. A fixed left two-column shell: dark left sidebar (`.app-nav`) with
|
|
|
72
81
|
persona-filtered nav in the middle, pinned footer with the user's name + a ghost "Log out"
|
|
73
82
|
button; content area on the right. **No top header bar** (see Critical rules).
|
|
74
83
|
|
|
84
|
+
**All colour comes from a semantic token layer, `assets/css/theme.css`**, registered **first**
|
|
85
|
+
in `App_FrameworkIndex_Tools::__construct()` so every other sheet resolves through it. The app
|
|
86
|
+
supports light / dark / auto; the resolved theme is stamped on `<html>` as `data-theme` by an
|
|
87
|
+
inline pre-paint script and the user's choice lives in `localStorage` (`toga-tools-theme`) —
|
|
88
|
+
**not** the DB. The left sidebar and public header stay dark in **both** themes, which is why
|
|
89
|
+
`toga-brandmark-white.png` still works everywhere. Login shows `toga-horizontal-colored.png` in
|
|
90
|
+
light and `toga-horizontal-white.png` in dark. See `features/theme-light-dark.md`.
|
|
91
|
+
|
|
75
92
|
## Home route
|
|
76
93
|
|
|
77
94
|
`/` (`mvc/get.php`) renders a persona-filtered dashboard of available tools (tiles grouped by
|
|
@@ -106,6 +123,11 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
|
|
|
106
123
|
composer package must `require_once` the autoloader itself.
|
|
107
124
|
|
|
108
125
|
## Change history
|
|
126
|
+
- 2026-08-04 — Added the app-wide light/dark/auto theme: `assets/css/theme.css` token layer
|
|
127
|
+
registered first in `App_FrameworkIndex_Tools`, pre-paint `data-theme` stamping via the
|
|
128
|
+
one-and-only `setHeaderCode()` hook, `localStorage` preference (no DB column), nav/public
|
|
129
|
+
header dark in both themes. Recorded that `style.scss` is dead and `setHeaderCode()`
|
|
130
|
+
sets-not-appends. Not committed to `tools` yet (jcardinal)
|
|
109
131
|
- 2026-07-24 — Fixed the config-file web-root exposure: `config.*.ini` was publicly readable via
|
|
110
132
|
the `!-f`-only front controller (`GET /config.production.ini` served it verbatim); added
|
|
111
133
|
`<FilesMatch>` deny blocks referencing the new 1.0 back-end security standard. Softened the
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [kyalamarthi]
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: [kyalamarthi, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/clickup/sprint.php
|
|
13
13
|
- tools/v2/sprints/tile/index.php
|
|
@@ -24,6 +24,7 @@ related:
|
|
|
24
24
|
- ./persona-gated-navigation.md
|
|
25
25
|
- ./saml-sso-auth.md
|
|
26
26
|
- ./mvc-data-access-patterns.md
|
|
27
|
+
- ./theme-light-dark.md
|
|
27
28
|
- ../architecture.md
|
|
28
29
|
- ../../../2.0/apps/worker2/features/team-sprint-management.md
|
|
29
30
|
---
|
|
@@ -138,5 +139,15 @@ the React dashboard supersedes it.
|
|
|
138
139
|
passed by reference", which the E_ALL error handler promotes to a fatal 500. Assign first:
|
|
139
140
|
`$res = App_Database::query(...); buildArrayOfRows($res, false);`.
|
|
140
141
|
|
|
142
|
+
- **The old Power BI embed page (`mvc/clickup/dashboard/get.php`) keeps ~6 fixed colour
|
|
143
|
+
literals on purpose — do NOT theme them.** Its card/frame colour `#0b1020` is matched to the
|
|
144
|
+
embedded Power BI report canvas so nothing flashes white while the iframe loads. Themed
|
|
145
|
+
surfaces there would reintroduce the flash in one theme or the other. Everything else on the
|
|
146
|
+
page was tokenised for the app-wide theme — see
|
|
147
|
+
[App-Wide Colour Theme](./theme-light-dark.md).
|
|
148
|
+
|
|
141
149
|
## Change history
|
|
150
|
+
- 2026-08-04 — Tokenised the `/clickup/dashboard` page-local styles for the app-wide light/dark
|
|
151
|
+
theme, keeping ~6 deliberate fixed literals (incl. the `#0b1020` frame that matches the
|
|
152
|
+
embedded Power BI canvas so the iframe load does not flash white). Not committed. (jcardinal)
|
|
142
153
|
- 2026-07-29 — Built `/clickup/react` as a native tools feature (data engine, `/v2/sprints/*` routes, CDN-React TV UI, iframe embed, "Dashboard/Clickup" nav); removed the old `/clickup/dashboard` Power BI nav entry; documented two reusable 1.0 gotchas (queryLogging preflight on Defaults-less DBs, buildArrayOfRows by-ref). (kyalamarthi)
|
|
@@ -208,5 +208,4 @@ None — internal/shared design-team tool.
|
|
|
208
208
|
- [Tools Architecture](../architecture.md)
|
|
209
209
|
- [Static Demo Hosting](../../../standalone/apps/forward/features/static-demo-hosting.md)
|
|
210
210
|
- [Deploying Tools to Elastic Beanstalk (AL2023)](../workflows/deploy-to-elastic-beanstalk-al2023.md)
|
|
211
|
-
|
|
212
|
-
</invoke>
|
|
211
|
+
|
|
@@ -6,14 +6,15 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [bala]
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: [bala, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/mvc/email-migration/notify/get.php
|
|
13
13
|
- tools/mvc/email-migration/notify/post.php
|
|
14
14
|
- tools/_/app/nav.php
|
|
15
15
|
related:
|
|
16
16
|
- ./mvc-data-access-patterns.md
|
|
17
|
+
- ./theme-light-dark.md
|
|
17
18
|
- ../workflows/deploy-to-elastic-beanstalk-al2023.md
|
|
18
19
|
- ../architecture.md
|
|
19
20
|
---
|
|
@@ -104,7 +105,19 @@ endpoint that swallows non-fatal diagnostics (returns `true`) for the duration o
|
|
|
104
105
|
fatals and thrown exceptions still reach the `catch`. The framework mechanism is documented in
|
|
105
106
|
[mvc-data-access-patterns](./mvc-data-access-patterns.md).
|
|
106
107
|
|
|
108
|
+
## Gotcha — the colour literals in `post.php` are EMAIL HTML; never tokenise them
|
|
109
|
+
|
|
110
|
+
`post.php` builds the digest **email** body. Email clients cannot resolve CSS custom
|
|
111
|
+
properties, so its colour literals must stay hard-coded — a `var(--c-…)` there renders as no
|
|
112
|
+
colour at all. This is easy to get wrong because the on-screen `get.php` in the same folder
|
|
113
|
+
*was* tokenised for the app-wide theme (see
|
|
114
|
+
[App-Wide Colour Theme](./theme-light-dark.md)). Rule: styles that leave the browser are not
|
|
115
|
+
themed.
|
|
116
|
+
|
|
107
117
|
## Change history
|
|
118
|
+
- 2026-08-04 — Tokenised the on-screen `get.php` page styles for the app-wide light/dark theme
|
|
119
|
+
and recorded that `post.php`'s email-HTML colour literals must stay hard-coded (email clients
|
|
120
|
+
cannot resolve CSS custom properties). Not committed. (jcardinal)
|
|
108
121
|
- 2026-08-03 — Built the Legacy Email Notifier: multi-file xlsx merge via vendored PhpSpreadsheet,
|
|
109
122
|
legacy-domain filter, `Client_True.Users` local-part resolution with always-domain-swap
|
|
110
123
|
(never-skip), one digest per person per legacy domain, and chunked (25/batch) browser-driven
|
|
@@ -20,6 +20,7 @@ files:
|
|
|
20
20
|
- tools/mvc/talos/settings/post.php
|
|
21
21
|
- tools/assets/css/style.css
|
|
22
22
|
related:
|
|
23
|
+
- ./theme-light-dark.md
|
|
23
24
|
- ./persona-gated-navigation.md
|
|
24
25
|
- ./mvc-data-access-patterns.md
|
|
25
26
|
- ../architecture.md
|
|
@@ -97,6 +98,20 @@ about cost; cost is driven by tool-call **intensity** via the profile
|
|
|
97
98
|
- **Voice multipliers (0.25 / 0.50) are UNVALIDATED** — supplied second-hand, never measured,
|
|
98
99
|
and possibly inverted. Flagged in the UI. **Do not quote voice from them.**
|
|
99
100
|
- **`db_team`** is read/write on Contracts and Settings, read-only on the dashboards.
|
|
101
|
+
- **Chart.js colours must come from theme tokens, and a live theme switch needs
|
|
102
|
+
`chart.update()`.** These pages now read series/axis/tick/gridline colours via
|
|
103
|
+
`TogaTheme.color('--c-…')` inside a `paint()` registered with `TogaTheme.onChange(paint)`,
|
|
104
|
+
then call `chart.update()` — `update()` clears Chart.js's resolver cache, so without it a
|
|
105
|
+
`Chart.defaults` change does not reach charts that already exist. Chart.js otherwise defaults
|
|
106
|
+
axis/tick text to a dark colour that is unreadable in dark mode. See
|
|
107
|
+
[App-Wide Colour Theme](./theme-light-dark.md).
|
|
108
|
+
- **These pages keep an explicit `--c-bs-*` token group** because they were authored against
|
|
109
|
+
Bootstrap's palette (`#0d6efd`, `#6c757d`, `#495057`, `#dc3545`, `#198754`…), which is close
|
|
110
|
+
to but not the TOGA brand palette — folding them into `--c-primary` visibly restyles them.
|
|
111
|
+
- **Open (dark mode):** the error toast measures ~2.4:1 (white on `--c-danger`) and likely
|
|
112
|
+
wants a fixed dark fill; the Pricing Dashboard target-band dataset never had a
|
|
113
|
+
`backgroundColor` (pre-existing authoring slip) so the band shades rather than tints; the
|
|
114
|
+
three chart pages cannot be statically proven light-identical and still need a browser pass.
|
|
100
115
|
|
|
101
116
|
## Security
|
|
102
117
|
- `config.*.ini` carries committed plaintext secrets (team-accepted, see architecture Known
|
|
@@ -104,5 +119,10 @@ about cost; cost is driven by tool-call **intensity** via the profile
|
|
|
104
119
|
(`[api] talos_backend_url` / `talos_backend_key`); no values recorded. Do not add secrets.
|
|
105
120
|
|
|
106
121
|
## Change history
|
|
122
|
+
- 2026-08-04 — Tokenised these pages for the new app-wide light/dark theme: page-local styles
|
|
123
|
+
now use `--c-*` tokens (Talos pages keep their own `--c-bs-*` Bootstrap ramp), and the three
|
|
124
|
+
Chart.js pages read colours via `TogaTheme.color()` + repaint on `TogaTheme.onChange()` with
|
|
125
|
+
`chart.update()`. Recorded the dark-mode open items (error-toast contrast, missing
|
|
126
|
+
target-band `backgroundColor`). Not committed. (jcardinal)
|
|
107
127
|
- 2026-08-04 — Restructured to **Contracts / Pricing Dashboard / Usage / Pricing Settings**. New Contracts pages replace Onboarding with a real PROSPECT/ACTIVE/CHURNED lifecycle (signed contracts churn, never hard-delete — `TalosPricingBands` cascades; post-signature edits append to `TalosContractAmendments`), plus actual-vs-estimate variance, month history chart and one-click apply-recommended-org-fee. Settings replaces the Cost Factors editor with measured rows read-only (`UPDATE ... AND isDerived = 0`). Benchmarks page deleted. Pricing Dashboard rebuilt for sales leadership. Fixed `usage.php clientOptions()` reading an always-empty `TalosClients` (selector rendered empty) → `GET /api/usage/clients` with Team-DB fallback; added cost-intensity panel. Estimator moved to token unit price + workload profiles. Wrote the missing `.app-content:has(.tool--wide)` rule and consolidated Talos CSS into `style.css`. (jcardinal)
|
|
108
128
|
- 2026-06-29 — Built the Talos Pricing UI: nav folder + 4 pages (Onboarding w/ live JS estimate and contract-signing that locks the band schedule into `TalosClients`+`TalosPricingBands` in one txn; read-only Pricing Dashboard + Usage Benchmarks; technical-only Cost Factors editor) and `App_Talos_Estimator` (reads `TalosCostFactors`, derives conversations/user so sales need not enter it). Reads Team DB via new `db_team` connection. Offerings CHAT/VOICE_TO_VOICE/NATURAL_VOICE = 1.0/0.25/0.50 (voice ambiguous, deferred). (jcardinal)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: App-Wide Colour Theme (light / dark / auto)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- tools/assets/css/theme.css
|
|
13
|
+
- tools/assets/js/theme.js
|
|
14
|
+
- tools/_/app/frameworkindex.php
|
|
15
|
+
- tools/_/app/nav.php
|
|
16
|
+
- tools/common/header.php
|
|
17
|
+
- tools/mvc/login/get.php
|
|
18
|
+
- tools/assets/css/style.css
|
|
19
|
+
- tools/assets/css/design.css
|
|
20
|
+
- tools/assets/css/login.css
|
|
21
|
+
- tools/assets/css/cloudfront-clients.css
|
|
22
|
+
- tools/assets/css/gchart.css
|
|
23
|
+
- tools/mvc/talos/usage/get.php
|
|
24
|
+
- tools/mvc/talos/pricing/get.php
|
|
25
|
+
- tools/mvc/talos/contracts/get.php
|
|
26
|
+
- tools/mvc/clickup/dashboard/get.php
|
|
27
|
+
- tools/mvc/email-migration/notify/post.php
|
|
28
|
+
related:
|
|
29
|
+
- ../architecture.md
|
|
30
|
+
- ./persona-gated-navigation.md
|
|
31
|
+
- ./talos-pricing-ui.md
|
|
32
|
+
- ./clickup-sprint-dashboard.md
|
|
33
|
+
- ./legacy-email-notifier.md
|
|
34
|
+
- ./design-demo-admin.md
|
|
35
|
+
- ../../../standards/frontend.md
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Summary
|
|
39
|
+
|
|
40
|
+
A **light / dark / auto** colour theme for the *entire* Tools app, built as a single
|
|
41
|
+
semantic-token layer (`assets/css/theme.css`) rather than per-page edits. The user's choice
|
|
42
|
+
is a three-way switcher rendered next to their name in the left-nav footer, persisted in
|
|
43
|
+
**`localStorage` (`toga-tools-theme`) — deliberately not a DB column**, and applied
|
|
44
|
+
**before first paint** by an inline bootstrap script so there is no flash of the wrong theme.
|
|
45
|
+
|
|
46
|
+
**Light mode is a hard constraint: pixel-identical to before the change**, verified
|
|
47
|
+
mechanically (see [Verification](#verification-how-to-prove-light-mode-did-not-change)).
|
|
48
|
+
Any new page or stylesheet in this app should consume `--c-*` tokens and will then theme for
|
|
49
|
+
free.
|
|
50
|
+
|
|
51
|
+
> **Status (2026-08-04): NOT COMMITTED.** All of this lives in the working tree of the local
|
|
52
|
+
> `tools` checkout, which is on `_production` — it needs a feature branch before it lands.
|
|
53
|
+
> Verified **statically only**: resolver diffs, contrast maths, `php -l` on the 14 changed PHP
|
|
54
|
+
> files, `node --check` on `theme.js`. **No browser check and no specialist-reviewer pass has
|
|
55
|
+
> been run.** See [Open items](#open-items).
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
### 1. The token layer — `assets/css/theme.css`
|
|
60
|
+
|
|
61
|
+
~142 semantic CSS custom properties are declared on `:root` **with the original light
|
|
62
|
+
values**, and overridden in a `:root[data-theme="dark"]` block. It is registered **first** in
|
|
63
|
+
`App_FrameworkIndex_Tools::__construct()` so every other stylesheet resolves through it.
|
|
64
|
+
|
|
65
|
+
Token groups and their meaning:
|
|
66
|
+
|
|
67
|
+
| Group | Meaning |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `--c-surface*` | fills |
|
|
70
|
+
| `--c-text*` | foregrounds |
|
|
71
|
+
| `--c-border*` | borders |
|
|
72
|
+
| `--c-primary*` | brand (`-tNN` = alpha tint) |
|
|
73
|
+
| `--c-nav-*` | left nav / public header — **dark in BOTH themes** |
|
|
74
|
+
| `--c-on-accent`, `--c-on-dark-tNN` | white in BOTH themes; content sitting on a permanently dark/coloured fill |
|
|
75
|
+
| `--c-slate-*`, `--c-indigo-*`, `--c-green-*`, `--c-red-*` | secondary ramp used by `design.css` + `cloudfront-clients.css` |
|
|
76
|
+
| `--c-bs-*` | legacy Bootstrap ramp, Talos pages only (see gotchas) |
|
|
77
|
+
| `--c-code-bg`, `--c-code-fg` | fixed dark code slab (not themed) |
|
|
78
|
+
|
|
79
|
+
`color-scheme` is itself tokenised as `--c-color-scheme` so native scrollbars and form
|
|
80
|
+
controls follow the theme. Note `style.css` previously hard-coded `color-scheme: light` on
|
|
81
|
+
`html, body`, which beat the `:root` token — that had to be removed.
|
|
82
|
+
|
|
83
|
+
### 2. Pre-paint application (no flash)
|
|
84
|
+
|
|
85
|
+
`App_FrameworkIndex_Tools::themeBootstrapScript()` returns a tiny inline `<script>` injected
|
|
86
|
+
with `App_FrameworkIndex::setHeaderCode()`; `library` echoes it inside `<head>` **after** the
|
|
87
|
+
CSS/JS includes. It reads `localStorage` and stamps `data-theme` + `data-theme-pref` onto
|
|
88
|
+
`<html>` before the first paint. **An external JS file cannot do this** — it runs too late and
|
|
89
|
+
you get a flash of the wrong theme.
|
|
90
|
+
|
|
91
|
+
### 3. Runtime API — `assets/js/theme.js` (`window.TogaTheme`)
|
|
92
|
+
|
|
93
|
+
- `get()` → `'light' | 'dark'` (resolved theme)
|
|
94
|
+
- `getPreference()` → `'light' | 'dark' | 'system'`
|
|
95
|
+
- `set(pref)` — persists + applies
|
|
96
|
+
- `onChange(fn)` — **fires immediately with the current theme**, then on every change, so a
|
|
97
|
+
caller has one code path for both "colour yourself now" and "recolour"
|
|
98
|
+
- `color('--token')` — read a themed colour out of CSS for JS-driven visuals
|
|
99
|
+
|
|
100
|
+
Persistence: `localStorage` key **`toga-tools-theme`**, values `light|dark|system`. Every
|
|
101
|
+
access is `try`/`catch`ed — Safari private mode *throws* rather than returning `null`.
|
|
102
|
+
`system` follows the OS live via a `matchMedia('(prefers-color-scheme: dark)')` change
|
|
103
|
+
listener (with the `addListener` fallback for Safari <14), and a `storage` event listener keeps
|
|
104
|
+
multiple open tabs in agreement.
|
|
105
|
+
|
|
106
|
+
### 4. The switcher (server-rendered)
|
|
107
|
+
|
|
108
|
+
Rendered in `common/header.php` inside `.app-nav-footer` — next to the signed-in user's name,
|
|
109
|
+
above **Log out** — as a `role="radiogroup"` of three buttons carrying
|
|
110
|
+
`data-theme-choice="light|dark|system"` (labelled **Light / Dark / Auto**). `theme.js` manages
|
|
111
|
+
`aria-checked` + a roving `tabindex` and supports arrow keys. Icons come from a new
|
|
112
|
+
`App_Nav::themeIcon($theme)` that returns **inline SVG** from a fixed 3-key allowlist — inline
|
|
113
|
+
so the glyph inherits `currentColor`.
|
|
114
|
+
|
|
115
|
+
### 5. Login-page wordmark
|
|
116
|
+
|
|
117
|
+
Two `<img>` wordmarks are in the markup and CSS shows one per theme
|
|
118
|
+
(`.login-logo--light` / `.login-logo--dark`, keyed off `:root[data-theme="dark"]`):
|
|
119
|
+
`toga-horizontal-colored.png` in light, `toga-horizontal-white.png` in dark.
|
|
120
|
+
|
|
121
|
+
Chosen over swapping `src` in JS or using `<picture>`: CSS honours an **explicit** Light/Dark
|
|
122
|
+
choice, whereas a `<picture>` media query only sees `prefers-color-scheme` and would ignore a
|
|
123
|
+
forced override — and CSS cannot flash the wrong logo. Both PNGs are 3610×700 so there is no
|
|
124
|
+
reflow. Tradeoff: the browser downloads both (~49 KB extra).
|
|
125
|
+
|
|
126
|
+
### 6. Chart.js pages
|
|
127
|
+
|
|
128
|
+
`talos/usage`, `talos/pricing` and `talos/contracts` read series/axis/tick/gridline colours
|
|
129
|
+
from tokens via `TogaTheme.color()` inside a `paint()` function registered with
|
|
130
|
+
`TogaTheme.onChange(paint)`, followed by `chart.update()`. **`chart.update()` is required** —
|
|
131
|
+
it clears Chart.js's resolver cache so `Chart.defaults` changes take effect on charts that
|
|
132
|
+
already exist. Without this, Chart.js defaults axis/tick text to a dark colour that is
|
|
133
|
+
unreadable on a dark background.
|
|
134
|
+
|
|
135
|
+
## Decisions
|
|
136
|
+
|
|
137
|
+
- **Left nav and public header stay dark in BOTH themes** (deepened for dark). Preserves the
|
|
138
|
+
app's identity, guarantees zero light-mode regression there, and keeps the existing white
|
|
139
|
+
brandmark PNG usable in both themes.
|
|
140
|
+
- **Preference is browser-local (`localStorage`), not a DB column** — explicit developer
|
|
141
|
+
requirement.
|
|
142
|
+
- **Light mode must be pixel-identical**, proven mechanically rather than by eye.
|
|
143
|
+
- **`assets/css/style.scss` left untouched** — neither revived nor deleted (out of scope; that
|
|
144
|
+
is already the status quo — see gotcha below).
|
|
145
|
+
|
|
146
|
+
## Verification (how to prove light mode did not change)
|
|
147
|
+
|
|
148
|
+
1. **Resolver diff.** Resolve every `var(--c-*)` back through the `:root` (light) block and
|
|
149
|
+
diff the result against the pre-change file. Identical output = light mode unchanged.
|
|
150
|
+
2. **Dark-coverage assert.** Every `:root` token must have a `[data-theme="dark"]` override
|
|
151
|
+
unless it is intentionally fixed.
|
|
152
|
+
3. **Per-file colour multiset vs. `git HEAD`.** Compare the *multiset* of resolved colour
|
|
153
|
+
values per file, not a line-by-line diff — an index/line-based diff is useless once lines
|
|
154
|
+
are inserted, whereas a multiset comparison is immune to line-number shifts.
|
|
155
|
+
|
|
156
|
+
## Gotchas
|
|
157
|
+
|
|
158
|
+
- **`assets/css/style.scss` is DEAD in this repo — edit `style.css` directly.** Many recent
|
|
159
|
+
commits touch the `.css` while the `.scss` was last touched in an old ticket; the `.css` has
|
|
160
|
+
drifted ~450 lines ahead. There is no sass binary and no `package.json`. "Helpfully"
|
|
161
|
+
recompiling the SCSS would **silently revert a large amount of shipped work**.
|
|
162
|
+
- **`App_FrameworkIndex::setHeaderCode()` is the only hook for injecting markup into `<head>`
|
|
163
|
+
in 1.0** (library echoes `$this->headerCode` inside `<head>` after the CSS/JS includes). It
|
|
164
|
+
**SETS rather than appends**, and before this feature it was unused anywhere in tools — so a
|
|
165
|
+
**second caller silently clobbers the first**. If another feature needs head markup, refactor
|
|
166
|
+
to compose both strings; do not add a second `setHeaderCode()` call.
|
|
167
|
+
- **A colour literal used as both a foreground and a background must be split into two
|
|
168
|
+
tokens**, or dark mode inverts it the wrong way. Real cases in tools: `#10113A` (body text
|
|
169
|
+
*and* nav/public-header background), `#20242c` (text-strong *and* a dark chip fill), `#999`
|
|
170
|
+
(text grey *and* a badge fill), `#eef0f4` (border *and* badge fill), `#0f172a` (design.css
|
|
171
|
+
`--d-ink` text *and* the code-block background). Detect these **mechanically** — collect, per
|
|
172
|
+
candidate token, the set of CSS properties it appears with (background vs. colour/border).
|
|
173
|
+
Do not eyeball it.
|
|
174
|
+
- **A brand colour needs TWO tokens in a dark theme.** `--c-primary` (text/borders on a panel)
|
|
175
|
+
is **lightened** in dark for legibility; `--c-primary-fill` (a fill behind white text) is
|
|
176
|
+
kept **dark enough** in dark mode to preserve contrast. The roles pull in opposite
|
|
177
|
+
directions — one token makes white-on-brand buttons fail contrast.
|
|
178
|
+
- **The Talos admin pages keep their own `--c-bs-*` group.** They were authored against
|
|
179
|
+
Bootstrap's palette (`#0d6efd`, `#6c757d`, `#495057`, `#dc3545`, `#198754`…), which is close
|
|
180
|
+
to but **not** the TOGA brand palette; folding them into `--c-primary` visibly restyles those
|
|
181
|
+
pages. Only map a literal onto a token whose light value is **identical**.
|
|
182
|
+
- **`mvc/email-migration/notify/post.php` generates EMAIL HTML — its colour literals must stay
|
|
183
|
+
hard-coded.** Email clients cannot resolve CSS custom properties. It is the same file family
|
|
184
|
+
as the on-screen page, so it is easy to tokenise by mistake.
|
|
185
|
+
- **`mvc/clickup/dashboard/get.php` keeps ~6 fixed literals on purpose.** Its card/frame colour
|
|
186
|
+
`#0b1020` matches the embedded Power BI report canvas so nothing flashes white while the
|
|
187
|
+
iframe loads. Do not theme those.
|
|
188
|
+
|
|
189
|
+
## Open items
|
|
190
|
+
|
|
191
|
+
- The three Chart.js pages **cannot** be statically proven light-identical — their colours are
|
|
192
|
+
now generated at runtime. Needs a browser pass.
|
|
193
|
+
- The Talos error toast measures ~2.4:1 in dark (white text on `--c-danger`) and probably wants
|
|
194
|
+
a fixed dark fill.
|
|
195
|
+
- `talos/pricing`'s target-band dataset never had a `backgroundColor` (a pre-existing authoring
|
|
196
|
+
slip), so its band renders as a faint darkening rather than a green tint.
|
|
197
|
+
- A few Talos overlay shadows shifted from black-based to slate-based (imperceptible but real),
|
|
198
|
+
and 7 imperceptible colour substitutions were accepted on Talos pages (largest:
|
|
199
|
+
`#f8f9fa` → `#f8f9fb`).
|
|
200
|
+
|
|
201
|
+
## Change history
|
|
202
|
+
- 2026-08-04 — Built the app-wide light/dark/auto theme: `assets/css/theme.css` (~142 `--c-*`
|
|
203
|
+
tokens on `:root` + a `[data-theme="dark"]` override, registered first in
|
|
204
|
+
`App_FrameworkIndex_Tools`), `assets/js/theme.js` (`window.TogaTheme`), a pre-paint inline
|
|
205
|
+
bootstrap via `setHeaderCode()`, a Light/Dark/Auto radiogroup in the nav footer
|
|
206
|
+
(`App_Nav::themeIcon()`), per-theme login wordmarks, and token-driven Chart.js repaint on the
|
|
207
|
+
three Talos chart pages. Preference in `localStorage` (`toga-tools-theme`), no DB column; nav
|
|
208
|
+
+ public header stay dark in both themes; light mode proven unchanged by resolver diff.
|
|
209
|
+
**Not committed — working tree on `_production`, no browser verification yet.** (jcardinal)
|
|
@@ -5,12 +5,13 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-08-04
|
|
9
9
|
owners: [jcardinal]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ./backend-php.md
|
|
13
13
|
- ./framework-rules.md
|
|
14
|
+
- ../apps/tools/features/theme-light-dark.md
|
|
14
15
|
- ../apps/tools/features/design-demo-admin.md
|
|
15
16
|
- ../apps/tools/features/talos-kb-documents-admin.md
|
|
16
17
|
---
|
|
@@ -66,10 +67,48 @@ Each round trip adds a level. Equality comparisons then fail too, because a fres
|
|
|
66
67
|
- `tools/_/app/design/github.php` — `App_Design_Github::decodeEntities()` applied to tab
|
|
67
68
|
names and project titles on both the storage-read and request-read sides.
|
|
68
69
|
|
|
70
|
+
## Tokenising an existing stylesheet for a second theme
|
|
71
|
+
|
|
72
|
+
When retrofitting light/dark onto a legacy sheet, four rules turn a guessing game into a
|
|
73
|
+
mechanical exercise.
|
|
74
|
+
|
|
75
|
+
**1. Split any literal used as both a foreground and a background into two tokens.** A single
|
|
76
|
+
token inverts the wrong way in the second theme. Detect these **mechanically** — collect, per
|
|
77
|
+
candidate token, the set of CSS properties it appears with (`background*` vs.
|
|
78
|
+
`color`/`border*`) — do not eyeball it. Real cases in `tools`: `#10113A` (body text *and* nav
|
|
79
|
+
background), `#20242c`, `#999`, `#eef0f4`, `#0f172a`.
|
|
80
|
+
|
|
81
|
+
**2. A brand colour needs TWO tokens.** `--c-primary` (text/borders on a panel) must be
|
|
82
|
+
**lightened** in dark for legibility; `--c-primary-fill` (a fill behind white text) must stay
|
|
83
|
+
**dark enough** in dark to preserve contrast. The roles pull in opposite directions; one token
|
|
84
|
+
makes white-on-brand buttons fail contrast.
|
|
85
|
+
|
|
86
|
+
**3. Only map a literal onto a token whose light value is IDENTICAL.** Measure perceptual
|
|
87
|
+
distance before accepting a "close enough" substitution — a near-match silently restyles the
|
|
88
|
+
page. Legacy pages authored against a *different* palette (the Talos admin pages use
|
|
89
|
+
Bootstrap's `#0d6efd`/`#6c757d`/`#dc3545`/`#198754`) keep their own token group with their
|
|
90
|
+
exact original values rather than being folded into the brand tokens.
|
|
91
|
+
|
|
92
|
+
**4. Prove the original theme is unchanged, mechanically.** (a) Resolve every `var(--…)` back
|
|
93
|
+
through the `:root` block and diff against the pre-change file; (b) assert every `:root` token
|
|
94
|
+
has a second-theme override unless intentionally fixed; (c) compare the per-file **multiset**
|
|
95
|
+
of resolved colour values against `git HEAD` — a line-index diff is useless once lines are
|
|
96
|
+
inserted, a multiset comparison is immune to line shifts.
|
|
97
|
+
|
|
98
|
+
Two things are **never** tokenised: HTML destined for an **email** (email clients cannot
|
|
99
|
+
resolve custom properties) and colours deliberately matched to third-party embedded content
|
|
100
|
+
(e.g. an iframe's canvas colour, to prevent a white flash on load).
|
|
101
|
+
|
|
102
|
+
Reference implementation:
|
|
103
|
+
[tools App-Wide Colour Theme](../apps/tools/features/theme-light-dark.md).
|
|
104
|
+
|
|
69
105
|
## Change history
|
|
70
106
|
|
|
107
|
+
- 2026-08-04 — Added "Tokenising an existing stylesheet for a second theme": split
|
|
108
|
+
dual-role colour literals, two tokens per brand colour, identical-value-only substitutions,
|
|
109
|
+
and the mechanical proof that the original theme is unchanged; plus the never-tokenise cases
|
|
110
|
+
(email HTML, colours matched to third-party embeds). Drawn from the `tools` light/dark
|
|
111
|
+
build. (jcardinal)
|
|
71
112
|
- 2026-07-30 — Created. Establishes the escape-once/plain-text-storage invariant after a
|
|
72
113
|
second independent occurrence in `tools` (Design Demo Admin, following the Talos
|
|
73
114
|
vocabulary tool). (jcardinal)
|
|
74
|
-
</content>
|
|
75
|
-
</invoke>
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: ["mhammontree", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -82,6 +82,50 @@ tenant's other APIs at all. See
|
|
|
82
82
|
> result above is authoritative; the code path is misleading. Anyone tempted to reason it out
|
|
83
83
|
> from V2.php will reach the wrong answer.
|
|
84
84
|
|
|
85
|
+
## The dispatch is UNGUARDED — a row naming a method that does not exist hard-fatals the endpoint
|
|
86
|
+
|
|
87
|
+
The derived name (step 2 above) is **pure convention with nothing validating it**, and there is
|
|
88
|
+
**no `phpMethod` column in production** to override it. All **four** call sites in
|
|
89
|
+
`api2/Component/Api/V2/V2.php` invoke the derived name **without a `method_exists()` check**:
|
|
90
|
+
|
|
91
|
+
| Site | Phase |
|
|
92
|
+
|------|-------|
|
|
93
|
+
| `~L8655` | builds the interceptor lookup |
|
|
94
|
+
| `~L3148` | pre-processing dispatch |
|
|
95
|
+
| `~L5694` | post-processing dispatch |
|
|
96
|
+
| `~L7411` / `~L7437` | alternate pre-processing path |
|
|
97
|
+
|
|
98
|
+
So a **single `ApiPayloadInterceptors` row whose (recordId, phase, method) resolves to a method the
|
|
99
|
+
model does not define takes the endpoint down** with a PHP fatal —
|
|
100
|
+
`Call to undefined method _Model_<Slug>_<Model>::postPost()` — not a handled 4xx/5xx. This is the
|
|
101
|
+
mirror image of the "missing row" failure mode above: a *missing* row is silent, an *extra/wrong*
|
|
102
|
+
row is fatal.
|
|
103
|
+
|
|
104
|
+
**Fix direction:** guard all four sites with `method_exists()` and raise a logged `Logs.Issue`
|
|
105
|
+
instead of letting the request fatal.
|
|
106
|
+
|
|
107
|
+
### The client-model resolution that decides which class must define the method
|
|
108
|
+
|
|
109
|
+
At `~L5681-5690` the engine derives the concrete class by `str_replace`-ing `_Model_Client_` →
|
|
110
|
+
`_Model_<jwt client slug>_` on `$record->model`, guarded by `class_exists`. For Compass USA that
|
|
111
|
+
yields `_Model_Compass_Usa_*`.
|
|
112
|
+
|
|
113
|
+
The chain is `_Model_Compass_Usa_X extends _Model_Compass_X extends _Model_Client_X`, so **a method
|
|
114
|
+
defined on the shared Compass base is inherited by both the Usa and Canada subclasses** — a
|
|
115
|
+
per-region subclass does *not* need its own copy. Do not add duplicate hooks per region.
|
|
116
|
+
|
|
117
|
+
### `Core.Records` facts for interceptor debugging
|
|
118
|
+
|
|
119
|
+
- **`Core.Records.model` is UNIQUE** — exactly one record row per model class, so an interceptor's
|
|
120
|
+
`recordId` maps to precisely one model. No ambiguity to resolve.
|
|
121
|
+
- The column is **`route`** (not `routePlural` / `routeSingular`) in the current schema. Example:
|
|
122
|
+
`recordId 15` = route `sales-order-items` = `_Model_Client_SalesOrderItem`.
|
|
123
|
+
- **Diagnostic:** join the client's active interceptor rows to `Core.Records` and check each derived
|
|
124
|
+
method actually exists in `_underscore/Model/<Slug>/`. Compass's legitimate rows (records **14**
|
|
125
|
+
sales-orders, **17** purchase-orders, **19** vendor-items, **21** items, **37** contacts, **55**
|
|
126
|
+
advance-shipping-notices, **179** approval-decisions) all map 1:1 to real methods — **the row whose
|
|
127
|
+
record has no corresponding method is the anomaly.**
|
|
128
|
+
|
|
85
129
|
## Worked example — the EV-10 that was not a code bug
|
|
86
130
|
|
|
87
131
|
`_Model_Client_ItemFulfillment::prePost()` defaults `itemFulfillmentStageId` to the shipped stage on
|
|
@@ -99,6 +143,14 @@ and the failing environment**. It is a small table, and the drift is usually exa
|
|
|
99
143
|
- **⚠ A hook that "isn't running" is a missing registration row before it is a code bug.** Check
|
|
100
144
|
`ApiPayloadInterceptors` for `(recordId, phase, method)` **first** — never start by re-reading the
|
|
101
145
|
PHP.
|
|
146
|
+
- **⚠ An interceptor row for a method that does not exist is a production outage, not a no-op.**
|
|
147
|
+
The dispatch is unguarded at all four call sites — the endpoint fatals with *"Call to undefined
|
|
148
|
+
method"*. If a write endpoint suddenly 500s for one client only, list that client's
|
|
149
|
+
`ApiPayloadInterceptors` rows and confirm every derived method exists.
|
|
150
|
+
- **⚠ A migration that enables an interceptor must insert `isActive = 0`.** Activation is a *data*
|
|
151
|
+
change that switches on a *code* path; if the PHP defining the method is not confirmed deployed to
|
|
152
|
+
that environment, the row takes the endpoint down. Insert inactive, verify the deploy, then flip
|
|
153
|
+
`isActive = 1` — and remember every environment activates independently.
|
|
102
154
|
- **The method name is derived, so a typo'd enum silently misses.** A row with
|
|
103
155
|
`prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine
|
|
104
156
|
will simply find no method and move on.
|
|
@@ -114,6 +166,17 @@ and the failing environment**. It is a small table, and the drift is usually exa
|
|
|
114
166
|
|
|
115
167
|
## Change history
|
|
116
168
|
|
|
169
|
+
- 2026-08-04 — Documented the **inverse failure mode**: the derived-name dispatch is **unguarded at
|
|
170
|
+
all four `V2.php` call sites** (`~L8655` lookup, `~L3148` pre, `~L5694` post, `~L7411`/`~L7437`
|
|
171
|
+
alternate pre) and there is **no `phpMethod` column in production**, so one row naming a
|
|
172
|
+
nonexistent method hard-fatals the endpoint (*"Call to undefined method
|
|
173
|
+
`_Model_Compass_Usa_SalesOrderItem::postPost()`"*). Added the client-model resolution at
|
|
174
|
+
`~L5681-5690` (`_Model_Client_` → `_Model_<slug>_`, `class_exists`-guarded) and the
|
|
175
|
+
`Usa → Compass → Client` inheritance rule (define once on the Compass base; no per-region copy),
|
|
176
|
+
the `Core.Records` debugging facts (`model` is UNIQUE, column is `route`, record 15 =
|
|
177
|
+
`sales-order-items`, Compass's 1:1 legitimate row set), and the rule that a migration enabling an
|
|
178
|
+
interceptor must land `isActive = 0` first. Diagnosed from a production incident; no code change.
|
|
179
|
+
(dfranks)
|
|
117
180
|
- 2026-08-03 — Added two verified behaviours from a live dev probe: (1) interceptor registration
|
|
118
181
|
is **per-API** (`apiId` on the row, in both the Core and `Client_<X>` tables), so a guard in a
|
|
119
182
|
hook is scoped by **DB config rather than code** and a new row widens its blast radius; (2) the
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: ["mhammontree", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -15,7 +15,9 @@ files:
|
|
|
15
15
|
- _underscore/Model/Core/Logs/Api.php
|
|
16
16
|
related:
|
|
17
17
|
- ../architecture.md
|
|
18
|
+
- ./api-payload-interceptors.md
|
|
18
19
|
- ../../../../clients/aig/features/entitlement-intake.md
|
|
20
|
+
- ../../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
## Summary
|
|
@@ -61,6 +63,28 @@ That is the deciding factor when choosing between `_Exception_Validation` (400,
|
|
|
61
63
|
request, no Issue) and `_Exception_Business` (500, Sentry + a routed Issue). Do **not** reach for
|
|
62
64
|
`_Exception_Business` merely to "make sure the rejection is recorded" — it already is.
|
|
63
65
|
|
|
66
|
+
### ⚠ But an uncaught PHP **fatal** can leave NO log row anywhere
|
|
67
|
+
|
|
68
|
+
The commit-the-logs / roll-back-the-data contract above holds for the **handled** paths — a
|
|
69
|
+
validation rejection or an exception the controller catches. It did **not** hold for an **uncaught
|
|
70
|
+
`Error`** observed in production on 2026-08-04 (a `Call to undefined method` raised from an
|
|
71
|
+
[API payload interceptor](api-payload-interceptors.md) dispatch): the fatal was reported to the
|
|
72
|
+
error tracker, but **`Logs_Compass.Api` contained zero 5xx rows for the entire day and the failing
|
|
73
|
+
request appears nowhere in it at all.** Suspected regression in the recent `Error.php` /
|
|
74
|
+
Issue-Event table normalization work.
|
|
75
|
+
|
|
76
|
+
Practical consequences:
|
|
77
|
+
|
|
78
|
+
- **Do not conclude "the request never happened" from an empty log table.** For a fatal, absence of
|
|
79
|
+
a log row is not evidence of absence of the request. Check the error tracker's per-event detail —
|
|
80
|
+
that may be the *only* record of the individual request.
|
|
81
|
+
- **Diagnose from the DATA, not the log.** The data transaction rolls back while a *previous*
|
|
82
|
+
successful request already committed its own rows, so a fatal on a multi-request write sequence
|
|
83
|
+
leaves a detectable **orphan / zero-child parent**. Sweep for those instead of grepping logs — see
|
|
84
|
+
[Compass Order Lifecycle & Data-Integrity Invariants](../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md#detecting-empty-shell-orders-left-by-a-fatal-mid-sequence).
|
|
85
|
+
- **Orphan sweeps only find PARTIAL casualties.** An attempt that rolled back completely leaves no
|
|
86
|
+
row in any table and, with no API log row, no trail at all.
|
|
87
|
+
|
|
64
88
|
## Blind spots when auditing a client's API traffic
|
|
65
89
|
|
|
66
90
|
Looking only at `Logs_<Client>.Api` misses four sources:
|
|
@@ -110,6 +134,12 @@ re-send.
|
|
|
110
134
|
|
|
111
135
|
## Change history
|
|
112
136
|
|
|
137
|
+
- 2026-08-04 — Added the limit of the commit-logs contract: an **uncaught PHP fatal** (a
|
|
138
|
+
`Call to undefined method` from an interceptor dispatch) produced **zero rows in
|
|
139
|
+
`Logs_Compass.Api`** — no 5xx row for the whole day and no row for the failing request — while
|
|
140
|
+
still reaching the error tracker. Suspected regression in the `Error.php` / Issue-Event
|
|
141
|
+
normalization work. So log-based diagnosis fails for this class of 500; sweep the **data** for
|
|
142
|
+
zero-child parents instead, and accept that fully-rolled-back attempts leave no trail. (dfranks)
|
|
113
143
|
- 2026-08-03 — Corrected a common wrong assumption: an **HTTP 400 leaves a full log row**.
|
|
114
144
|
`Controller/Index.php` L369-377 commits `DB_LOGS`/`DB_CLIENT_LOGS` and rolls back only the
|
|
115
145
|
business transaction, so rejections are queryable and attributable to the credential; what they
|
package/knowledge/INDEX.md
CHANGED
|
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
7
|
- **library** (Library) _(framework core)_ — 15 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
|
-
- **worker** (Worker) —
|
|
8
|
+
- **worker** (Worker) — 20 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
11
11
|
- **togadesk** (TOGa Desk) — 11 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
@@ -14,7 +14,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
15
15
|
- **test** (Test) — 14 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
|
-
- **tools** (Tools) —
|
|
17
|
+
- **tools** (Tools) — 14 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
@@ -12,5 +12,6 @@
|
|
|
12
12
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
13
13
|
| [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
|
|
14
14
|
| [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
|
|
15
|
-
| [Compass ODP
|
|
15
|
+
| [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
|
|
16
|
+
| [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json, library/app/client/compass.php |
|
|
16
17
|
| [Compass Order Lifecycle & Data-Integrity Invariants](workflows/order-lifecycle-and-data-integrity.md) | 2.0 | End-to-end map of how a Compass order flows through the `Client_Compass` (2.0) database and the **expected raw-data shape** at each link/ASN/IF level. | |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: compass-usa
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: ["dfranks"]
|
|
11
|
+
files:
|
|
12
|
+
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
13
|
+
- worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php
|
|
14
|
+
- worker/schedules/cron.worker.sync.json
|
|
15
|
+
related:
|
|
16
|
+
- clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md
|
|
17
|
+
- clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
|
|
18
|
+
- ../../../2.0/apps/api2/features/api-payload-interceptors.md
|
|
19
|
+
- ../../../2.0/apps/api2/features/request-logging.md
|
|
20
|
+
- clients/compass-usa/profile.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron
|
|
26
|
+
**3a** created the ODP SalesOrder header, the follow-up item write died (e.g. an api2 fatal), and 3a
|
|
27
|
+
then **deleted the S3 object**, leaving a zero-item order and nothing to retry.
|
|
28
|
+
|
|
29
|
+
The recovery works because the raw x12 is archived in `Logs.FileLog` and **3a is fully idempotent**:
|
|
30
|
+
re-uploading the file makes it repair the items, the ODP PO, and both bridges. Verified end-to-end
|
|
31
|
+
in **production** on 2026-08-04 for ODP PO `41608261-1135` / ODP SO `475390815001`.
|
|
32
|
+
|
|
33
|
+
## When to use this
|
|
34
|
+
|
|
35
|
+
Symptoms: a zero-item ODP SalesOrder (see the orphan sweeps in
|
|
36
|
+
[order lifecycle & data integrity](order-lifecycle-and-data-integrity.md)), and cron 5 re-attempting
|
|
37
|
+
the NetSuite SO every 10 minutes because it selects on `c_dtTransmittedToNetsuite IS NULL`.
|
|
38
|
+
|
|
39
|
+
## The runbook
|
|
40
|
+
|
|
41
|
+
### Step 1 — retrieve the archived x12
|
|
42
|
+
|
|
43
|
+
The raw EDI is on the **legacy-core cluster**, table **`Logs.FileLog`**:
|
|
44
|
+
|
|
45
|
+
- `job = 'ODP_EDI'`
|
|
46
|
+
- `fileName = '<odpPoNumber>.x12'` — note **`.x12`**, even though the inbound S3 filename is
|
|
47
|
+
`EDI_<odpPoNumber>.txt`.
|
|
48
|
+
|
|
49
|
+
**Query it bounded by `job` + `fileTimestamp`.** An unbounded `fileName LIKE` scan times out on this
|
|
50
|
+
table.
|
|
51
|
+
|
|
52
|
+
Extract with `mysql --raw --batch --skip-column-names`, then **strip the trailing newline the client
|
|
53
|
+
appends** so the byte count matches the stored `LENGTH(fileData)` exactly. A byte-count mismatch means
|
|
54
|
+
you have a corrupted payload — do not upload it.
|
|
55
|
+
|
|
56
|
+
### Step 2 — know that S3 has nothing left
|
|
57
|
+
|
|
58
|
+
3a **deletes the S3 object after processing** (`:769`), so a failed import leaves no file to retry.
|
|
59
|
+
Recovery is a **re-upload**, to:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
s3://agilant-as2/OfficeDepot/EDI_<odpPoNumber>.txt
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Step 3 — clear the cause BEFORE re-uploading
|
|
66
|
+
|
|
67
|
+
**Confirm no `ApiPayloadInterceptors` row exists for the `sales-order-items` record** (`recordId 15`)
|
|
68
|
+
whose derived method the model does not define. If one is still active, the item write fatals again
|
|
69
|
+
**and the file is deleted a second time**. See
|
|
70
|
+
[API payload interceptors](../../../2.0/apps/api2/features/api-payload-interceptors.md).
|
|
71
|
+
|
|
72
|
+
### Step 4 — let 3a repair it (≤ 5 minutes)
|
|
73
|
+
|
|
74
|
+
3a runs `*/5`. Because every step is existence-guarded it **reuses** the existing SO and **backfills**
|
|
75
|
+
what is missing: unmatched items (matched case-insensitively by `partNumber`), the ODP PO if absent,
|
|
76
|
+
and both the `PurchaseOrders_SalesOrders` and `PurchaseOrderItems_SalesOrderItems` bridges. No
|
|
77
|
+
duplicates are created.
|
|
78
|
+
|
|
79
|
+
**Then set `customerPurchaseOrder` manually** — 3a never updates the SalesOrder *header* on an
|
|
80
|
+
existing order, so it stays `NULL`. Prefer direct SQL over a `PUT`; the reasoning and its two
|
|
81
|
+
tradeoffs are in the
|
|
82
|
+
[lifecycle doc](order-lifecycle-and-data-integrity.md#repairing-a-header-only-scalar-field-direct-sql-vs-put).
|
|
83
|
+
|
|
84
|
+
### Step 5 — let cron 5 transmit to NetSuite
|
|
85
|
+
|
|
86
|
+
Cron 5 (`*/10`) picks the now-complete ODP SO up and stamps `c_dtTransmittedToNetsuite` +
|
|
87
|
+
`c_netsuiteInternalSalesOrderId`. It excludes `number LIKE 'MA%'`. Its stamps landing is the
|
|
88
|
+
confirmation the recovery finished.
|
|
89
|
+
|
|
90
|
+
## Gotchas / known issues
|
|
91
|
+
|
|
92
|
+
- **⚠ Re-uploading before fixing the root cause destroys the file again** (3a deletes on the way
|
|
93
|
+
through). Step 3 is not optional.
|
|
94
|
+
- **⚠ The archive filename extension differs from the S3 one** — `.x12` in `Logs.FileLog`,
|
|
95
|
+
`EDI_<po>.txt` in S3. Searching for the wrong one finds nothing.
|
|
96
|
+
- **Don't search for the ODP SalesOrder by the `BEG` number.** `BEG` is the ODP *PO*; the SO number
|
|
97
|
+
is `REF~QC`. Full segment map in the
|
|
98
|
+
[ODP pipeline doc](odp-order-pipeline-to-netsuite.md).
|
|
99
|
+
- **`Logs.FileLog` has limited retention** (~30 days) — beyond that there is no archived x12 and the
|
|
100
|
+
recovery must come from ODP re-sending.
|
|
101
|
+
- A **fully** rolled-back import leaves no row anywhere and no API log row, so it will not show up in
|
|
102
|
+
an orphan sweep at all; only the error tracker's per-event detail can enumerate those.
|
|
103
|
+
|
|
104
|
+
## Change history
|
|
105
|
+
|
|
106
|
+
- 2026-08-04 — Documented the runbook after recovering a production ODP order whose item write was
|
|
107
|
+
killed by an api2 fatal: `Logs.FileLog` (`job = 'ODP_EDI'`, `<po>.x12`, bounded by `fileTimestamp`)
|
|
108
|
+
as the x12 archive, the trailing-newline/byte-count check, the re-upload path to
|
|
109
|
+
`s3://agilant-as2/OfficeDepot/`, the mandatory "clear the interceptor first" step, 3a's idempotent
|
|
110
|
+
repair plus the `customerPurchaseOrder` header gap, and cron 5's `*/10` stamps as the completion
|
|
111
|
+
signal. (dfranks)
|
|
@@ -6,16 +6,18 @@ project: Worker
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["rgirish", "bala"]
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: ["rgirish", "bala", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
13
13
|
- worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php
|
|
14
14
|
- worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php
|
|
15
15
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
16
16
|
- worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php
|
|
17
|
+
- worker/schedules/cron.worker.sync.json
|
|
17
18
|
- library/app/client/compass.php
|
|
18
19
|
related:
|
|
20
|
+
- clients/compass-usa/workflows/odp-edi-import-recovery.md
|
|
19
21
|
- clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
|
|
20
22
|
- clients/compass-usa/features/mits-po-transmission-to-vendors.md
|
|
21
23
|
- clients/compass-usa/features/mits-po-to-so-item-linking.md
|
|
@@ -40,7 +42,8 @@ Commerce → Compass SalesOrder (SalesOrders.customerId = 2)
|
|
|
40
42
|
MITS → PurchaseOrder (PurchaseOrders.vendorId = 1 = OFFICE DEPOT; POST /v2/purchase-orders; PO number e.g. 50305906-1)
|
|
41
43
|
[cron 2_transmit_mits_purchase_orders_to_vendors] → ODP (cXML; email fallback per integration) [sets PurchaseOrders.dtSubmitted]
|
|
42
44
|
ODP → 850 EDI dropped to S3 (AS2) (bucket agilant-as2, prefix OfficeDepot/)
|
|
43
|
-
[cron
|
|
45
|
+
[cron workflow/3a_import_office_depot_purchase_orders — NOT edi/1, see correction below]
|
|
46
|
+
→ creates ODP SalesOrder (customerId = 1); deletes the S3 file after processing
|
|
44
47
|
ODP SalesOrder (customerId = 1)
|
|
45
48
|
[cron 5_create_netsuite_sales_orders_from_office_depot_purchase_orders, hourly] → NetSuite SO
|
|
46
49
|
writes back c_dtTransmittedToNetsuite + c_netsuiteInternalSalesOrderId ONTO the ODP SO (customerId=1)
|
|
@@ -56,12 +59,57 @@ ODP SalesOrder (customerId = 1)
|
|
|
56
59
|
`PurchaseOrders.dtSubmitted` when transmitted (~2 min after PO creation). (See the dedicated
|
|
57
60
|
[MITS PO Transmission to Vendors](../features/mits-po-transmission-to-vendors.md) feature for
|
|
58
61
|
the item-less-PO gotcha and the CXML-vs-EMAIL routing detail.)
|
|
59
|
-
3. **`
|
|
60
|
-
`agilant-as2`, prefix `OfficeDepot/` (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
3. **`workflow/3a_import_office_depot_purchase_orders.php`** — pulls the ODP **850** from S3 bucket
|
|
63
|
+
`agilant-as2`, prefix `OfficeDepot/` (excluding `OUTBOX/` and `SENT/`; `:46` S3 read,
|
|
64
|
+
`:54-58` prefix filters) and creates the downstream **ODP SalesOrder** (customerId=1 = Office
|
|
65
|
+
Depot). **Deletes the S3 object after processing** (`:769`) — so an absent S3 object is expected
|
|
66
|
+
once ingested, not evidence of a miss, *and* a failed import leaves nothing to retry.
|
|
67
|
+
|
|
68
|
+
> **⚠ Correction (2026-08-04): `edi/1_download_edi_s3_create_po_toga.php` is DEAD CODE.** It
|
|
69
|
+
> appears in **no** schedule file. The scheduled entry *named* "Download EDI From S3 Files &
|
|
70
|
+
> Create PO - EDI Office Depot" (`*/5 * * * *`, `worker/schedules/cron.worker.sync.json`
|
|
71
|
+
> `:254-259`) actually points at **`toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`**.
|
|
72
|
+
> The two scripts have **different idempotency and different API call shapes** — `edi/1`'s create
|
|
73
|
+
> branch POSTs the SO with *nested* items and a `customer.name`, while **3a POSTs the header alone
|
|
74
|
+
> (uuids) and then POSTs the items in a separate request**. Reasoning from `edi/1` gives wrong
|
|
75
|
+
> answers about what production does.
|
|
76
|
+
>
|
|
77
|
+
> **General rule: never infer which script runs from its filename or folder.** Verify against
|
|
78
|
+
> `worker/schedules/cron.*.json` — and note the schedule entry's **name can be actively
|
|
79
|
+
> misleading**.
|
|
80
|
+
|
|
81
|
+
### 3a is idempotent — re-dropping an EDI file is safe
|
|
82
|
+
|
|
83
|
+
Every step of 3a is gated by a `totalRecordCount == 0` existence check, so a re-drop produces **no
|
|
84
|
+
duplicates and backfills whatever is missing**:
|
|
85
|
+
|
|
86
|
+
| Step | Guard | Behavior on re-run |
|
|
87
|
+
|------|-------|--------------------|
|
|
88
|
+
| ODP SalesOrder | `:162-227` existence check | reuses the existing SO |
|
|
89
|
+
| SO items | `:470-498` | matches existing items **case-insensitively by `partNumber`**; creates only unmatched ones |
|
|
90
|
+
| ODP PurchaseOrder | `:356-436` | creates only if missing |
|
|
91
|
+
| `PurchaseOrders_SalesOrders` + `PurchaseOrderItems_SalesOrderItems` bridges | `:632` | creates only if missing |
|
|
92
|
+
|
|
93
|
+
**KNOWN GAP: 3a never updates the SalesOrder HEADER on an existing order.** So
|
|
94
|
+
`customerPurchaseOrder` stays `NULL` on a repaired order and must be set manually. See
|
|
95
|
+
[Recovering a lost ODP EDI import](odp-edi-import-recovery.md).
|
|
96
|
+
|
|
97
|
+
### The EDI 850 → TOGa identifier mapping (not guessable — this is the key to tracing an ODP order)
|
|
98
|
+
|
|
99
|
+
| 850 segment | Meaning | Where it lands |
|
|
100
|
+
|-------------|---------|----------------|
|
|
101
|
+
| **`BEG`** | the **Office Depot PO number** (e.g. `41608261-1135`) | `PurchaseOrders.number`, vendor "Agilant Solutions Inc" |
|
|
102
|
+
| **`REF~QC`** | the **ODP SALES ORDER number** (e.g. `475390815001`) | `SalesOrders.number` with `customerId = 1` |
|
|
103
|
+
| **`REF~EU` / `REF~PO`** | the **Compass MITS PO number** (e.g. `50310002-1`) | its `c_mitsSalesOrder` field holds the ODP SO number |
|
|
104
|
+
| **`REF~LU`** | the **Compass sales order** (e.g. `SA135273`) | `SalesOrders` with `customerId = 2` |
|
|
105
|
+
| **`PO1`** | the line | qty, UOM, unit price, vendor part, ODP item id |
|
|
106
|
+
|
|
107
|
+
**⚠ `REF~QC` — not `BEG` — is the value to search on for the ODP SalesOrder.** Searching
|
|
108
|
+
`PurchaseOrders` / `SalesOrders` for the `BEG` number finds nothing for the SO and wastes time.
|
|
109
|
+
(This is the segment-level detail behind the "don't search by the MITS PO number" rule below.)
|
|
110
|
+
4. **`workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php`** — scheduled
|
|
111
|
+
**every 10 minutes** (`*/10`, verified against `worker/schedules/` on 2026-08-04; this doc
|
|
112
|
+
previously said "hourly"); picks up ODP SalesOrders
|
|
65
113
|
(`OfficeDepotSalesOrders.customerId = 1 AND c_dtTransmittedToNetsuite IS NULL`), creates the
|
|
66
114
|
SO in NetSuite, and writes back `c_dtTransmittedToNetsuite` + `c_netsuiteInternalSalesOrderId`
|
|
67
115
|
**onto the ODP SalesOrder (customerId=1), never the Compass SO (customerId=2)**.
|
|
@@ -70,6 +118,10 @@ ODP SalesOrder (customerId = 1)
|
|
|
70
118
|
- **computer-kit orders** (Bundles 187–196) wait for a **2nd PO** before syncing.
|
|
71
119
|
- a **30-minute delay** after the first ODP PO is created.
|
|
72
120
|
|
|
121
|
+
Because the selection is only `c_dtTransmittedToNetsuite IS NULL` (plus `number NOT LIKE 'MA%'`),
|
|
122
|
+
a **zero-item ODP SalesOrder sits in this queue retrying every 10 minutes** until its lines are
|
|
123
|
+
restored — the retry loop is the symptom, not the cause.
|
|
124
|
+
|
|
73
125
|
## Cron 5 NetSuite SO creation — failure modes & the two ODP error emails
|
|
74
126
|
|
|
75
127
|
Two different Compass ODP error emails exist, from **two different pipeline stages**. They look
|
|
@@ -141,6 +193,15 @@ wrong record makes every order look "stuck." Use the **join**, never a number ma
|
|
|
141
193
|
transmission feature.)
|
|
142
194
|
|
|
143
195
|
## Change history
|
|
196
|
+
- 2026-08-04 — **Corrected the 850-import step**: `edi/1_download_edi_s3_create_po_toga.php` is
|
|
197
|
+
**dead code in no schedule**; the schedule entry named "Download EDI From S3 Files & Create PO -
|
|
198
|
+
EDI Office Depot" (`*/5`) actually runs `workflow/3a_import_office_depot_purchase_orders.php`,
|
|
199
|
+
which has different idempotency and posts the SO header and its items as **two separate API
|
|
200
|
+
requests**. Added 3a's full existence-check/idempotency map (re-dropping a file is safe; header is
|
|
201
|
+
never updated so `customerPurchaseOrder` stays NULL), the **EDI 850 → TOGa identifier mapping**
|
|
202
|
+
(`BEG` = ODP PO, `REF~QC` = ODP SO — the one to search on, `REF~EU`/`REF~PO` = MITS PO,
|
|
203
|
+
`REF~LU` = Compass SO), and corrected cron 5's schedule to `*/10`. Production incident
|
|
204
|
+
investigation; no code change. (dfranks)
|
|
144
205
|
- 2026-08-03 — Documented cron 5's "Please enter a value for Ext Price" root cause (`rate` set
|
|
145
206
|
only for NetSuite item groups, never plain items → NetSuite `add()` USER_ERROR, order re-emails
|
|
146
207
|
hourly while stuck), the two distinct ODP error emails (3a PO-import vs 5 SO-creation) and cron
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: workflow
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
9
|
-
owners: ["jcardinal", "bala"]
|
|
8
|
+
updated: 2026-08-04
|
|
9
|
+
owners: ["jcardinal", "bala", "dfranks"]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- clients/compass-usa/profile.md
|
|
@@ -93,6 +93,41 @@ one-upstream for bundles.
|
|
|
93
93
|
5. **Mirror back-links:** every IF on a non-source SO (ODP/Agilant) should have
|
|
94
94
|
`upstreamItemFulfillmentId` set, and each of its items `upstreamItemFulfillmentItemId` set.
|
|
95
95
|
|
|
96
|
+
## Detecting "empty shell" orders left by a fatal mid-sequence
|
|
97
|
+
|
|
98
|
+
The ODP EDI import creates a SalesOrder **header** and its **line items** in **separate API
|
|
99
|
+
requests** (`POST /sales-orders`, then `POST /sales-order-items` ~4s later — see the
|
|
100
|
+
[ODP pipeline doc](odp-order-pipeline-to-netsuite.md)). If the *second* request dies, the header is
|
|
101
|
+
already committed and the items roll back, leaving a **zero-item sales order**. This is the
|
|
102
|
+
signature to sweep for whenever an api2 write fatals — and, because a fatal can leave **no API log
|
|
103
|
+
row at all** ([request logging](../../../2.0/apps/api2/features/request-logging.md)), the **data is
|
|
104
|
+
the only trail**.
|
|
105
|
+
|
|
106
|
+
Sweeps against `Client_Compass`:
|
|
107
|
+
|
|
108
|
+
1. **Zero-item sales orders** — `SalesOrders LEFT JOIN SalesOrderItems … GROUP BY so.id HAVING
|
|
109
|
+
COUNT(soi.id) = 0`.
|
|
110
|
+
2. **Zero-item purchase orders** — the same shape over `PurchaseOrders` / `PurchaseOrderItems`.
|
|
111
|
+
3. **ODP SOs with no upstream PO** — ODP SOs (`customerId = 1`) `LEFT JOIN
|
|
112
|
+
PurchaseOrders_SalesOrders WHERE` the bridge row `IS NULL`.
|
|
113
|
+
|
|
114
|
+
> **⚠ LIMITATION: this finds only PARTIAL casualties.** An attempt that rolled back *completely*
|
|
115
|
+
> leaves no row in any table, and with no API log row there is no trail anywhere — only the error
|
|
116
|
+
> tracker's **per-event detail** can enumerate those. Never report an orphan sweep as a complete
|
|
117
|
+
> casualty list.
|
|
118
|
+
|
|
119
|
+
### Repairing a header-only scalar field: direct SQL vs `PUT`
|
|
120
|
+
|
|
121
|
+
When only a scalar needs setting on an existing order (e.g. `customerPurchaseOrder`, which the ODP
|
|
122
|
+
importer never backfills), **direct SQL is the deliberate choice over `PUT /sales-orders/{uuid}`**: a
|
|
123
|
+
`PUT` fires the record-14 `POST`/`PUT` [payload
|
|
124
|
+
interceptor](../../../2.0/apps/api2/features/api-payload-interceptors.md) and drags the whole order
|
|
125
|
+
back through MITS/NetSuite logic for one field.
|
|
126
|
+
|
|
127
|
+
Accept the two tradeoffs: **no API log row** for the change, and **`dtUpdated` still moves** (the
|
|
128
|
+
column is `ON UPDATE CURRENT_TIMESTAMP`), which can look like a fresh modification to anything doing
|
|
129
|
+
`dtUpdated`-based change detection downstream.
|
|
130
|
+
|
|
96
131
|
## Verified schema corrections (older code/scripts assume these wrongly)
|
|
97
132
|
- **`ItemFulfillmentPackages` does NOT exist** — header IF tracking is `ItemFulfillments_TrackingNumbers`.
|
|
98
133
|
- `ItemFulfillmentItemUnits` has **no `trackingNumberId`** — unit tracking is the bridge
|
|
@@ -120,6 +155,13 @@ runtime workflow.
|
|
|
120
155
|
- High-multiplier over-fulfillment (5×–20×) does not fit the split-PO spurious-link pattern — separate cause.
|
|
121
156
|
|
|
122
157
|
## Change history
|
|
158
|
+
- 2026-08-04 — Added the **"empty shell" order signature** — the ODP importer writes the SO header
|
|
159
|
+
and its items in two separate API requests, so a fatal on the second leaves a committed,
|
|
160
|
+
zero-item order — plus the three orphan sweeps that detect it (zero-item SOs, zero-item POs, ODP
|
|
161
|
+
SOs with no `PurchaseOrders_SalesOrders` bridge) and the limitation that they only find *partial*
|
|
162
|
+
casualties. Also recorded why a header-only scalar repair uses **direct SQL over `PUT`** (a `PUT`
|
|
163
|
+
fires the record-14 interceptor and the whole MITS/NetSuite path) and its two tradeoffs (no API
|
|
164
|
+
log row; `dtUpdated` still moves via `ON UPDATE CURRENT_TIMESTAMP`). (dfranks)
|
|
123
165
|
- 2026-06-30 — Updated the IF-stage invariant: `itemFulfillmentStageId` is now NOT NULL and
|
|
124
166
|
picked/packed/shipped stages are all in use (resolved by slug), superseding "stage 3 = Shipped;
|
|
125
167
|
stages 1/2 unused". Compass order status remains shipped-only. (bala)
|
package/package.json
CHANGED