toga-ai 1.0.510 → 1.0.512

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.
@@ -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-07-24
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-07-29
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
- </content>
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-03
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-07-30
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>
@@ -364,6 +364,18 @@ in `_underscore` and `library` in the **same** release, and the `/errors` consol
364
364
  `dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql`. Do not reintroduce it.
365
365
  - **Shared-schema migration collision.** Because these tables live in the *shared* Core Logs
366
366
  DB, only **one** migration may `CREATE` them; later migrations only `ALTER`.
367
+ - **The `dbchanges2/Logs/` migration files LAG the deployed schema — read the live cluster, not the
368
+ migrations.** `dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql` does **not**
369
+ describe the live `Logs.Issue`: notably the identity column is **`issueKey` (varchar 128)**, not
370
+ `code`. Live columns include `reference`, `dtLastOccurred`, `isManaged`, `urgency`,
371
+ `minimumUrgency`, `consecutiveQuietWindows`, `subject`, `errorMessage`, `occurrences`,
372
+ `totalOccurrences`, `recurrenceCount`, `dtLastNotified`, `notificationCount`, `dtAcknowledged`,
373
+ `dtMutedUntil`, `createsClickupTask`, `clickupPriority`, `clickupAssigneeId`. Query
374
+ `information_schema` (or `SHOW TABLES` / `SHOW COLUMNS`) against the live Logs cluster **before**
375
+ writing any SQL against this feature — reading the migrations instead produced confidently wrong
376
+ conclusions twice on 2026-08-04. Same trap for the **singular** table names
377
+ (`Issue`/`Event`/`IssueFingerprint`/`IssueAreaOwner`/`IssueClickupTask`/`IssueEmailAddress`,
378
+ renamed by `2026-08-01a` and confirmed applied in prod).
367
379
  - **Pre-existing committed secrets in this area (not fixed — out of scope, report only).**
368
380
  A ClickUp API token at `_underscore/Component/Api/Clickup/Clickup.php:5`; SES SMTP
369
381
  credentials at `_underscore/Email.php:11-12`; a named individual's Sentry personal access
@@ -418,6 +430,12 @@ clientUserId). **Neither was built.** As built instead:
418
430
 
419
431
  ## Change history
420
432
 
433
+ - 2026-08-04 — **Correction/diagnostic:** recorded that the `dbchanges2/Logs/` migration files lag
434
+ the deployed schema — `2026-07-30a` describes a `code` identity column, but live `Logs.Issue` is
435
+ keyed on **`issueKey` (varchar 128)** — and listed the live column set. Query
436
+ `information_schema` against the live Logs cluster before writing SQL here; trusting the
437
+ migrations (and forgetting the applied singular rename) produced two confidently wrong
438
+ conclusions in one session. No code change. (dfranks)
421
439
  - 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Reference encoding reworked
422
440
  to base 32** over `'0123456789ACDEFGHJKMNPQRSTUVWXYZ'` (all digits kept; `O`/`I`/`L`/`B` dropped
423
441
  as impersonators of `0`/`1`/`1`/`8`), replacing the base-26 decimal-counter+letter scheme, with
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-08-04
10
- owners: ["jcardinal"]
10
+ owners: ["jcardinal", "dfranks"]
11
11
  files:
12
12
  - worker2/Worker/Infrastructure/Errors.php
13
13
  - worker2/Worker/Notification/Email.php
@@ -28,6 +28,7 @@ related:
28
28
  - ./clickup-project-routing.md
29
29
  - ./notification-email.md
30
30
  - ./notification-email-template.md
31
+ - ../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md
31
32
  ---
32
33
 
33
34
  ## Summary
@@ -263,6 +264,22 @@ this controller needs the same treatment.**
263
264
  issue gets a **neutral** subject rather than another client's raw error text.
264
265
  - **`IssueEmailAddress.clientId = 0` means "all clients"** and requires an explicit
265
266
  confirmation flag in the Tools console; list views render it as **"ALL CLIENTS"**.
267
+ - **A recipient scoped to a specific `clientId` delivers NOTHING for occurrences recorded without
268
+ client attribution.** `loadRecentClientIds()` maps an occurrence whose `clientId` is NULL to
269
+ **`0`**, and `resolveRecipientsForClient()` resolves scope `0` **plus** the erroring client. So
270
+ the rule is: scope to **`0`** when the `issueKey` is raised from a **single client-specific code
271
+ path** (a Compass-only cron, say); **never** scope to `0` for an `issueKey` raised from shared
272
+ code, which would leak one client's alerts to another's recipients.
273
+ - **Configuring a recipient is never retroactive.** `shouldNotify()` notifies on first occurrence
274
+ (`notificationCount` 0), on escalation, or when the backed-off reminder interval has elapsed —
275
+ and returns **false when the current window has zero events**. The first email therefore lands on
276
+ the **next live occurrence**, not on the backlog. Do not read silence right after a config change
277
+ as a failure.
278
+ - **Adding a business recipient is purely additive — it cannot switch ClickUp ticketing off.**
279
+ Routing is **both**, not either: `if ($recipients !== null)` emails the business recipients and
280
+ `if ($issue->createsClickupTask)` tickets the dev team, evaluated **independently**. The in-code
281
+ comment marking this records the earlier `elseif` form, where adding one recipient silently
282
+ disabled an issue's dev-team ticket.
266
283
  - **The ClickUp webhook has no HMAC signature verification** (pre-existing, unfixed). A forged
267
284
  POST can mark issues acknowledged — freezing the neglect axis — or resolve episodes. That is
268
285
  alert suppression against the error-reporting system itself. Fix before relying on this
@@ -273,6 +290,15 @@ this controller needs the same treatment.**
273
290
 
274
291
  ## Change history
275
292
 
293
+ - 2026-08-04 (verified against `origin/_production` + the prod Logs cluster) — Recorded the
294
+ **recipient-routing semantics** a developer needs before configuring a business alert: routing is
295
+ **both** business email and dev-team ClickUp, evaluated independently (adding a recipient is
296
+ purely additive); `IssueEmailAddress.clientId` scoping means a client-scoped row delivers nothing
297
+ for occurrences with NULL `clientId` (mapped to `0`), so single-client `issueKey`s scope to `0`
298
+ and shared-code `issueKey`s never do; and `shouldNotify()` makes configuration
299
+ **non-retroactive** — the first email lands on the next live occurrence. Also noted that prod
300
+ `Logs.IssueEmailAddress` was still empty, making Compass's MITS rejection the platform's first
301
+ business-routed issue. No code change. (dfranks)
276
302
  - 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Fixed: escalation email had
277
303
  never been sent, ever**, for two independent reasons. (A) `processTechnicalIssue()` returned early
278
304
  when the ClickUp task would read identically, and that return sat *above* the `if ($shouldNotify)`
@@ -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) — 13 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
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
 
@@ -39,7 +39,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
39
39
  - **togatech** (TOGA Technology Website) — 4 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
40
40
  - **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
41
41
  - **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
42
- - **claude** (Claude Harness) — 3 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
42
+ - **claude** (Claude Harness) — 4 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
43
43
 
44
44
  ## Clients
45
45
 
@@ -8,6 +8,7 @@
8
8
  | [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql, dbchanges2/Client/2026-07-15a - ExcludeFeeItemsFromItemFulfillmentsForSalesOrdersView.sql |
9
9
  | [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
10
10
  | [Compass MITS PO Transmission to Vendors](features/mits-po-transmission-to-vendors.md) | 2.0 | The 1.0 worker cron `2_transmit_mits_purchase_orders_to_vendors.php` transmits Compass PurchaseOrders to their vendors (Office Depot, Strategic Systems, Compass | worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compasscanada/workflow/2_transmit_mits_purchase_orders_to_vendors.php, library/app/client/compass.php |
11
+ | [Compass MITS Sales-Order Transmission — Rejection Alerting (Issue/Event, not email-in-cron)](features/mits-sales-order-transmission-alerting.md) | 2.0 | When MITS **rejects** a Compass sales order transmitted by the 1.0 cron `1_transmit_compass_sales_orders_to_mits.php`, the alert is no longer an email built ins | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker2/Worker/Infrastructure/Errors.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql |
11
12
  | [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
12
13
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
13
14
  | [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 |
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Compass MITS Sales-Order Transmission — Rejection Alerting (Issue/Event, not email-in-cron)
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: compass-usa
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-08-04
9
+ owners: ["dfranks"]
10
+ files:
11
+ - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
12
+ - worker2/Worker/Infrastructure/Errors.php
13
+ - _underscore/Model/Core/Logs/Issue.php
14
+ - _underscore/Model/Core/Logs/IssueEmailAddress.php
15
+ - dbchanges2/Logs/2026-08-04a - MITS rejection business recipients.sql
16
+ related:
17
+ - mits-po-transmission-to-vendors.md
18
+ - ../../../2.0/apps/worker2/features/error-escalation-cron.md
19
+ - ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
20
+ - ../../../1.0/apps/library/features/error-capture-1-0.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ When MITS **rejects** a Compass sales order transmitted by the 1.0 cron
26
+ `1_transmit_compass_sales_orders_to_mits.php`, the alert is no longer an email built inside the
27
+ cron. It is a **business exception** raised into the shared Logs Issue/Event pipeline, and its
28
+ recipients are **rows in `Logs.IssueEmailAddress`**.
29
+
30
+ **Consequence for anyone handed a ticket like "add <person> to the MITS error emails": there is
31
+ no PHP to change.** It is a one-row data change delivered as a `dbchanges2/Logs/` migration.
32
+
33
+ ## How it works
34
+
35
+ - The cron raises
36
+ `throw new App_Exception_Business('COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED', …, URGENCY_HIGH)`
37
+ on a MITS rejection. (Before 2026-08-03 this block was a hardcoded `new App_Email_Agilant()`
38
+ with `addTo('devteam@togatech.com')`.)
39
+ - Capture creates/dedupes a `Logs.Issue` keyed on the `issueKey`, and the worker2
40
+ `Infrastructure/Errors/Escalate` cron does all outbound notification — business email to the
41
+ configured recipients **and**, independently, the dev-team ClickUp task. See
42
+ [error-escalation-cron](../../../2.0/apps/worker2/features/error-escalation-cron.md).
43
+ - Recipients live in `Logs.IssueEmailAddress` (FK `issueId` → `Logs.Issue`), scoped by
44
+ `clientId` (`0` = all clients).
45
+
46
+ ### Adding a recipient (the correct pattern)
47
+
48
+ Write a `dbchanges2/Logs/YYYY-MM-DD<letter>` migration that:
49
+
50
+ 1. **Looks the issue up by `issueKey`**, never by its `id`. The id is assigned on the issue's
51
+ **first occurrence** and is therefore not stable across environments.
52
+ 2. Uses **`clientId = 0`**. This `issueKey` is raised from a Compass-only code path, and an
53
+ occurrence recorded without client attribution resolves against scope `0` only — a row scoped
54
+ to the Compass `clientId` can deliver nothing. (Never use `0` for an `issueKey` raised from
55
+ shared code — that is a cross-tenant leak.)
56
+ 3. Is **idempotent** via `INSERT … WHERE NOT EXISTS`, mirroring the unique key
57
+ `uq_issue_client_address_type`.
58
+
59
+ Configuration is **never retroactive**: `shouldNotify()` fires on the first occurrence, on
60
+ escalation, or when the backed-off reminder interval elapses, and returns false for a window with
61
+ zero events. The first email lands on the **next live rejection**, not on existing history.
62
+
63
+ ## Gotchas / known issues
64
+
65
+ - **Do not "modernize" this alert back into the cron.** Migrating it to
66
+ `_Model_Client_EmailTemplate::send` (or any in-cron email) reverts the move to the Issue/Event
67
+ pipeline and removes the recipient configuration surface entirely.
68
+ - **Never re-introduce the payload dump into the alert.** The original email included the
69
+ transmission payload, which carries ship-to addressee, attention line, street address and phone
70
+ number. The Issue message is stored in a table **shared across every client**, displayed to
71
+ internal staff in the Tools `/errors` console, pushed to ClickUp, and emailed — so the payload
72
+ was deliberately removed. Alerts here name the order, not the person.
73
+ - **The MITS cron holds its API credentials inline in the file.** Treat that file as
74
+ secret-bearing when copying snippets anywhere (tickets, PR bodies, knowledge docs).
75
+ - **Adding a recipient does not turn ClickUp ticketing off.** Routing is both-and, not
76
+ either-or — see the escalation-cron doc for the `elseif` bug this used to be.
77
+ - As of 2026-08-04 prod `Logs.IssueEmailAddress` is empty, so this is the **first business-routed
78
+ issue on the platform**: `processBusinessIssue()` will execute in production for the first time
79
+ when this issue next fires. Watch that run rather than assuming it works.
80
+
81
+ ## Change history
82
+ - 2026-08-04 — Documented that MITS rejection alerting moved (worker `5521854b`, 2026-08-03) from a
83
+ hardcoded in-cron `App_Email_Agilant` to `App_Exception_Business` +
84
+ `Logs.IssueEmailAddress` recipients, so adding an alert recipient is a data change, not a code
85
+ change. Recorded the deliberate removal of the PII-bearing payload dump from the alert, the
86
+ look-up-by-`issueKey` / `clientId = 0` / `NOT EXISTS` migration pattern, and that recipient
87
+ configuration only takes effect on the next live occurrence. (dfranks)
88
+ </content>
89
+ </invoke>
@@ -16,10 +16,11 @@ project: _Underscore
16
16
  client: compass-usa
17
17
  type: profile
18
18
  status: active
19
- updated: 2026-07-16
20
- owners: [jcardinal, bala, tcox, apeterson]
19
+ updated: 2026-08-04
20
+ owners: [jcardinal, bala, tcox, apeterson, dfranks]
21
21
  files: []
22
22
  related:
23
+ - features/mits-sales-order-transmission-alerting.md
23
24
  - features/asn-to-item-fulfillment.md
24
25
  - features/cost-centers.md
25
26
  - features/approval-decision-flow.md
@@ -75,6 +76,10 @@ separate, related client (see its own profile).
75
76
  `worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php`.
76
77
 
77
78
  ## Key features (this client)
79
+ - [MITS Sales-Order Transmission — Rejection Alerting](features/mits-sales-order-transmission-alerting.md)
80
+ — MITS rejection alerts are `App_Exception_Business` Issues with recipients in
81
+ `Logs.IssueEmailAddress`, **not** an email in the cron: adding an alert recipient is a
82
+ `dbchanges2/Logs/` data change with no PHP.
78
83
  - [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) — the
79
84
  `_Model_Compass_AdvanceShippingNotice::postPost` handler.
80
85
  - [Cost Centers (Unit Locations, numeric-only)](features/cost-centers.md) — what a Compass
@@ -5,3 +5,4 @@
5
5
  | [Harness Distribution — How a knowledge.js Fix Reaches Teammates](workflows/harness-distribution.md) | There are **two copies** of `knowledge.js` on a developer's machine, and the skills use only one of them. | claude/scripts/install.js, claude/knowledge.js |
6
6
  | [Knowledge Base Publish / Push Pipeline](workflows/knowledge-publish-pipeline.md) | How `/capture` and `/session-save` get knowledge docs from your working tree into `agilantsolutions/claude` on `_main`. | claude/knowledge.js |
7
7
  | [MCP Tool Usage — "Talos" Means the Internal Knowledge Base, and Run Your Own SELECTs](workflows/mcp-tool-usage.md) | Two MCP connections are available in every session, and **Claude is expected to reach for them on its own judgement — nobody should have to say the word "MCP".* | claude/.claude/skills/kickoff/SKILL.md, claude/.claude/skills/capture/SKILL.md |
8
+ | [Ticket-Branch Creation Safety — How /work-ticket Armed Pushes to Production Branches](workflows/ticket-branch-creation-safety.md) | For roughly a month, `/work-ticket` created ticket branches whose **git upstream was the production branch**. | claude/skills/work-ticket/SKILL.md |
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: Ticket-Branch Creation Safety — How /work-ticket Armed Pushes to Production Branches
3
+ framework: "standalone"
4
+ repo: claude
5
+ project: Claude Harness
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-04
10
+ owners: ["dfranks"]
11
+ files:
12
+ - claude/skills/work-ticket/SKILL.md
13
+ related:
14
+ - ./harness-distribution.md
15
+ - ../../../2.0/apps/worker2/workflows/ticket-to-pseudocode-planning.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ For roughly a month, `/work-ticket` created ticket branches whose **git upstream was the
21
+ production branch**. Claude's own pushes were unaffected, but a developer using VS Code's
22
+ **Commit + Sync** mid-session pushed ticket code straight to `_main`/`_production` — no PR, no
23
+ review. Two such pushes landed in two days before it was found.
24
+
25
+ The mechanism is generic git behavior, so it is worth understanding beyond this one skill.
26
+
27
+ ## Root cause
28
+
29
+ Branch creation was `git checkout -b TRUE-XXXX "origin/$DEFAULT"`. Git's
30
+ **`branch.autoSetupMerge` defaults to `true`**, so branching from a *remote-tracking ref* adopts
31
+ that ref as the new branch's **upstream**. The ticket branch therefore tracked
32
+ `origin/_main` / `origin/_production` until the skill's later `git push -u` re-pointed it.
33
+
34
+ **Trigger asymmetry is why it hid.** Claude pushes with `git push -u origin <TICKET>` — an
35
+ explicit refspec, which ignores the upstream and was always safe. VS Code's Commit + Sync resolves
36
+ the destination **from the upstream**, so any developer commit made before the skill's first push
37
+ went to the production branch.
38
+
39
+ **A safety fix created the exposure.** Commit `6d155f7` (2026-07-09) moved branch creation from
40
+ *after* implementation to *before* any code is written — itself a genuine fix, because a stale
41
+ local `_production` 12 commits behind nearly reverted upstream work. But it turned a seconds-long
42
+ armed window into the **entire implementation phase**. Correctness fixes can lengthen an existing
43
+ window; check what else depends on the timing you moved.
44
+
45
+ ## The fixes
46
+
47
+ 1. **Skill:** branch creation uses **`--no-track`** *and* publishes the empty branch immediately
48
+ (`git push -u origin TRUE-XXXX`), so the branch has a correct upstream before any code exists.
49
+ The later push is a plain `git push origin <TICKET>`.
50
+ 2. **Global git config:** `branch.autoSetupMerge=simple` (a checkout can no longer adopt a
51
+ production upstream, even outside the skill) plus `push.autoSetupRemote=true`.
52
+ 3. **`pre-push` hook** in each app repo rejecting
53
+ `refs/heads/(_main|_production|_beta|_alpha|master)`, override `ALLOW_PROD_PUSH=1`. This also
54
+ catches Claude, because Claude shells out to real `git`.
55
+ 4. **Deny rules** in `.claude/settings.json`.
56
+ 5. Existing branches still tracking a production branch were **disarmed** individually.
57
+
58
+ `rework-ticket` was never affected — it checks out an existing remote branch.
59
+
60
+ ## Recovery: getting an unreviewed commit off an unprotected default branch
61
+
62
+ Do **not** force-push. Instead:
63
+
64
+ 1. **Revert via PR** on the default branch.
65
+ 2. **Rebuild the ticket branch as a NEW commit** on the post-revert base.
66
+
67
+ > **The revert-remerge trap:** if you keep the original commit and merge that branch later, the
68
+ > original commit is already an **ancestor** of the default branch, so the merge is a silent no-op
69
+ > and the change **never comes back**. The revert wins and nothing warns you.
70
+
71
+ ## Gotchas / known issues
72
+
73
+ - **Everything installed here is local and does not survive a fresh clone.** Hooks and git config
74
+ are per-machine; a teammate or a new laptop is unprotected until they redo it.
75
+ - **Standing exposure (not fixed):** `dbchanges2/_main`, `worker/_production`,
76
+ `worker2/_production` and `api2/_production` have **no branch protection and no rulesets**.
77
+ Server-side protection is the only durable fix and needs an admin GitHub account.
78
+ - **Diagnosing this class of incident:** `git reflog` on the remote-tracking branch shows
79
+ `update by push` entries — that is how both incidents were confirmed and how the other four
80
+ repos were cleared. Pre-existing `_beta` pushes are normal manual practice; check dates before
81
+ calling something an incident.
82
+ - Auditing "did this ever happen elsewhere?" requires sweeping **every** repo the skill touches,
83
+ not just the one that surfaced the bug.
84
+
85
+ ## Change history
86
+ - 2026-08-04 — Documented the `branch.autoSetupMerge` root cause, the Claude-vs-VS-Code trigger
87
+ asymmetry that hid it for a month, and the fixes (`--no-track` + immediate branch publish,
88
+ global `autoSetupMerge=simple`, per-repo `pre-push` hook, settings deny rules). Recorded the
89
+ revert-then-rebuild recovery pattern and its revert-remerge no-op trap, plus the standing lack
90
+ of server-side branch protection on four app repos. (dfranks)
91
+ </content>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.510",
3
+ "version": "1.0.512",
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",