toga-ai 1.0.192 → 1.0.194

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.
@@ -6,13 +6,14 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-18
10
- owners: ["jcardinal", "bala"]
9
+ updated: 2026-06-24
10
+ owners: ["jcardinal", "bala", "mhammontree"]
11
11
  files:
12
12
  - _underscore/Model/Client/EmailTemplate.php
13
13
  - _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
14
14
  - _underscore/Email.php
15
- related: []
15
+ related:
16
+ - ../../worker2/features/notification-email.md
16
17
  ---
17
18
 
18
19
  ## Summary
@@ -33,6 +34,17 @@ sends via `_Email`. The only client-specific input the send actually needs is th
33
34
  non-API entry point. Caller passes the client identifier directly; no `$api` object.
34
35
  - **`dispatch(string $clientIdentifier, ...): bool`** (private) — the shared body both
35
36
  entry points call. Holds all the real logic.
37
+ - **`renderWrappedBody(string $subject, string $body): ?string`** — the **branded-wrapper**
38
+ path (distinct from the send-by-uuid path above). Loads the reserved wrapper row
39
+ (`uuid = WRAPPER_UUID`) and injects the subject/body into its `{subject}`/`{body}`
40
+ placeholders via `strtr` (simultaneous, so a `{body}` literal in the subject can't be
41
+ re-expanded). Returns `null` if there is no active wrapper row. Does **not** send — the caller
42
+ feeds the result to `_Email`.
43
+ - **`const WRAPPER_UUID = '11111111-1111-4111-8111-111111111111'`** — the reserved uuid of the
44
+ per-client branded "wrapper" template row. The model lookup and the dbchanges2 seed migration
45
+ must use this same literal. The row is seeded by dbchanges2 (`Client/` blank-client baseline so
46
+ every new client inherits it, plus per-client backfills). Its body is the branded HTML shell
47
+ holding `{subject}`/`{body}` placeholders.
36
48
  - `_Model_Client_EmailTemplateOutgoingEmailAddress` — per-template stored TO/CC/BCC
37
49
  addresses (`toCcBcc` enum), merged into the caller-supplied recipients.
38
50
  - `_underscore/Email.php` — `_Email` requires a non-empty `clientIdentifier` (throws
@@ -91,6 +103,11 @@ can keep using `sendEmail($api, ...)`.
91
103
 
92
104
  ## Change history
93
105
 
106
+ - 2026-06-24 — Added the **branded-wrapper** path: `WRAPPER_UUID` + `renderWrappedBody()` load a
107
+ reserved per-client `EmailTemplates` row and inject `{subject}`/`{body}`. Consumed by the new
108
+ worker2 `_Worker_Notification_Email::Send` for internal/notification mail; the wrapper row is
109
+ seeded across clients by dbchanges2. See
110
+ [`worker2/notification-email.md`](../../worker2/features/notification-email.md). (mhammontree)
94
111
  - 2026-06-18 — Hardened against silent failure: `dispatch()` throws on an inactive template
95
112
  (was `return false`) and `_Email::send()` throws + logs `isSuccess`/`error` when
96
113
  `PHPMailer::Send()` fails (was swallowed with no return/throw/log). Affects all clients;
@@ -3,3 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa View Frontend (toga2-view) Architecture](architecture.md) | `toga2-view` is the **React/TypeScript single-page frontend** for the TOGa 2.0 platform — the customer-facing web app (home warranty / tech-support portals). | toga2-view/src/main.tsx, toga2-view/src/App.tsx, toga2-view/src/routes.tsx, toga2-view/src/api/axiosInstance.ts, toga2-view/src/api/apiFunctions.ts, toga2-view/src/utils/queryHelpers.ts, toga2-view/src/contexts/AuthContext.tsx, toga2-view/src/contexts/useUserStore.ts, toga2-view/src/hooks/useAuthenticationFlow.ts, toga2-view/vite.config.ts, toga2-view/package.json |
6
+ | [Mobile Nav Header](features/mobile-nav-header.md) | The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. | toga2-view/src/components/MobileNav/MobileNav.tsx, toga2-view/public/assets/Rate_Logo.svg |
7
+ | [Service Card Component](features/service-card.md) | The `ServiceCard` component renders a single service subscription (tech support or home warranty) on both the Home and Services pages. | toga2-view/src/components/ServiceCard/ServiceCard.tsx, toga2-view/src/pages/Services/view/ServicesPage.tsx, toga2-view/src/pages/Home/view/HomePage.tsx, toga2-view/src/constants/bundleConstants.ts, toga2-view/src/pages/Services/viewModels/DUMMYFIELDS/SERVICESDUMMYFIELDS.json |
@@ -0,0 +1,53 @@
1
+ ---
2
+ title: Mobile Nav Header
3
+ framework: "2.0"
4
+ repo: toga2-view
5
+ project: TOGa View Frontend
6
+ client: rate
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-24
10
+ owners: ["bala"]
11
+ files:
12
+ - toga2-view/src/components/MobileNav/MobileNav.tsx
13
+ - toga2-view/public/assets/Rate_Logo.svg
14
+ related:
15
+ - clients/rate/profile.md
16
+ - 2.0/apps/toga2-view/features/service-card.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ The `MobileNavToggle` component renders the fixed 72 px header (hamburger + Rate logo) and the slide-in drawer nav. Header dimensions and spacing match Figma file `lQxDR8fCBXPnStWzI9BCv1`, node `9203-10323`.
22
+
23
+ ## Key files / entry points
24
+
25
+ - `MobileNav.tsx` — exports both `MobileNav` (drawer) and `MobileNavToggle` (page shell + fixed header)
26
+ - `public/assets/Rate_Logo.svg` — exported from Figma "Logos → Rate → Small" component (58.5 × 24 px); use SVG, not PNG, for correct aspect ratio
27
+
28
+ ## How it works
29
+
30
+ **Header layout (Figma spec):**
31
+ - Container: `fixed top-0 left-0 w-full z-50 bg-white h-[72px]`
32
+ - Inner flex row: `flex items-center h-[72px] px-4 gap-6`
33
+ - `px-4` = 16 px left/right (Figma X:16)
34
+ - `gap-6` = 24 px between hamburger and logo (derived from Figma: logo X:64 − hamburger X:16 − hamburger W:24 = 24 px)
35
+ - Hamburger button: `w-[24px] h-[24px]` at X:16, Y:24
36
+ - Rate logo: `<img src="./assets/Rate_Logo.svg" className="h-[24px] w-auto" />`
37
+ - Height 24 px → equal 24 px top/bottom padding in 72 px header (`items-center` on h-[72px])
38
+ - SVG scales to natural 58.5 px wide — matches Figma Logos component W:58.5 Hug H:24 Hug
39
+ - No `border-b`, no `shadow` on header (Figma has none)
40
+
41
+ **Drawer (MobileNav):**
42
+ - Logo in drawer: `w-[117px] h-[72px]` (3× the header logo, same `rateDark1.png`)
43
+ - Slide-in from left via framer-motion spring animation
44
+
45
+ ## Gotchas / known issues
46
+
47
+ - **Use `Rate_Logo.svg`, not `rateDark1.png` in the header.** The PNG has a 1.625:1 aspect ratio; at `h-[24px]` it renders only 39 px wide. The Figma Logos component is 58.5 × 24 px — only the SVG export matches this.
48
+ - **`EnvironmentBadge` is intentionally kept** in `App.tsx` — it is a dev/QA tool and must not be removed.
49
+ - **Page content top offset**: pages behind the fixed header need `mt-[72px]` to avoid overlap. The `allowedPaths` list in `MobileNavToggle` controls which routes get the nav shell.
50
+
51
+ ## Change history
52
+
53
+ - 2026-06-24 — Switched header logo to Rate_Logo.svg; fixed hamburger/logo spacing to match Figma (px-4, gap-6, h-[72px] items-center); removed border-b/shadow from header (bala)
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Service Card Component
3
+ framework: "2.0"
4
+ repo: toga2-view
5
+ project: TOGa View Frontend
6
+ client: rate
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-24
10
+ owners: ["bala"]
11
+ files:
12
+ - toga2-view/src/components/ServiceCard/ServiceCard.tsx
13
+ - toga2-view/src/pages/Services/view/ServicesPage.tsx
14
+ - toga2-view/src/pages/Home/view/HomePage.tsx
15
+ - toga2-view/src/constants/bundleConstants.ts
16
+ - toga2-view/src/pages/Services/viewModels/DUMMYFIELDS/SERVICESDUMMYFIELDS.json
17
+ related:
18
+ - clients/rate/profile.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ The `ServiceCard` component renders a single service subscription (tech support or home warranty) on both the Home and Services pages. It shows a hero image, title, Active badge, contract data rows, and a Manage button. Display names are normalised from raw API titles via a data-driven mapping.
24
+
25
+ ## Key files / entry points
26
+
27
+ - `ServiceCard.tsx` — presentational component; all sizing/spacing must match Figma node `9203-10283`
28
+ - `ServicesPage.tsx` — renders cards in a `flex flex-col gap-4 p-4` section (16 px uniform padding)
29
+ - `HomePage.tsx` — renders cards inside `DynamicSection`; content wrapper must have **no** extra `px-4` (outer `dynamicSectionContainer` already supplies 16 px)
30
+ - `bundleConstants.ts` — exports `mapServiceTitle()` which reads from `SERVICESDUMMYFIELDS.json`
31
+ - `SERVICESDUMMYFIELDS.json` — `serviceTitleMap` array; add/rename display names here without touching code
32
+
33
+ ## How it works
34
+
35
+ 1. View model calls `mapServiceTitle(bs.title)` before setting state — raw API title becomes the branded display name.
36
+ 2. `mapServiceTitle` iterates `serviceTitleMap` in the JSON, checking each entry's `keywords` array (lowercase includes-match). First match wins; unmatched titles pass through unchanged.
37
+ 3. `ServiceCard` renders with `bg-[#e9eff2] rounded-[8px] overflow-clip` (no border, no hover shadow).
38
+ 4. Hero image uses `aspect-[1482/604]` (40.75 % height ratio) — not the old padding-bottom hack.
39
+ 5. Card body: `flex flex-col gap-5 p-4` (gap = 20 px, padding = 16 px all sides per Figma).
40
+ 6. Data section (divider + rows): `flex flex-col gap-3` (12 px between divider and rows).
41
+ 7. Plan/Renews column: explicit `w-[91px]` — not `w-24` (96 px).
42
+ 8. Manage button: `p-3 rounded-full` (12 px uniform padding, `rounded-[99px]` equivalent).
43
+ 9. Divider: `border-t border-gray-300` (Figma token `Gray/gray-300`).
44
+
45
+ ## Data model
46
+
47
+ - `Client_Rate.Items.title` — raw service title (e.g. `1 Year Unlimited Tech Support - Annual`)
48
+ - Display names live in `SERVICESDUMMYFIELDS.json → serviceTitleMap`
49
+ - Current mappings:
50
+ - `"1 year unlimited tech support - annual"` → `Rate Whole Home Tech Services`
51
+ - `"whole home warranty - monthly"` → `Rate Whole Home Warranty`
52
+
53
+ ## Client variations
54
+
55
+ Rate-specific — title mapping and image paths are Rate-only. The `ServiceCard` component itself is generic.
56
+
57
+ ## Gotchas / known issues
58
+
59
+ - **Double padding on Home page**: `dynamicSectionContainer` has `px-4`; the services content wrapper inside `DynamicSection` must NOT add another `px-4` or cards appear too narrow.
60
+ - **`rounded-lg` ≠ exact Figma match**: use `rounded-[8px]` explicitly — the Tailwind config extends `borderRadius` with custom values that could shift defaults.
61
+ - **`overflow-hidden` vs `overflow-clip`**: Figma uses CSS `overflow: clip`; use Tailwind `overflow-clip` to match exactly.
62
+ - **`mapServiceTitle` uses the original API title for image matching** — pass `bs.title` (not the mapped title) to `determineServiceImage` so keyword matching still works.
63
+
64
+ ## Change history
65
+
66
+ - 2026-06-24 — Aligned all spacing/radius/overflow to Figma node 9203-10283; added data-driven title mapping from SERVICESDUMMYFIELDS.json; fixed double-padding on Home page (bala)
@@ -11,6 +11,7 @@
11
11
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
12
12
  | [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
13
13
  | [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
14
+ | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
14
15
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
15
16
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
16
17
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-06-18
10
- owners: [jcardinal, dfranks]
10
+ owners: [jcardinal, dfranks, mhammontree]
11
11
  files:
12
12
  - worker2/Worker/
13
13
  - worker2/Controller/Index.php
@@ -34,7 +34,10 @@ Lambda code changes are needed** — you just create the PHP file.
34
34
  - Method: `MethodName`
35
35
  - Example: `Client/Acme/ImportData` → `Worker/Client/Acme.php`, `_Worker_Client_Acme::ImportData`.
36
36
  2. **Parameters** — name + PHP type (`string`/`int`/`bool`/`array`/`float`); optional ones
37
- get PHP defaults. The framework spreads the `parameters` JSON as positional arguments.
37
+ get PHP defaults. The framework spreads the **string-keyed** `parameters` array into the method
38
+ (`$class::$method(...$parameters)`), which PHP binds as **named arguments** — so each method
39
+ parameter name *is* the queue's parameter contract. Do **not** collapse a multi-param action
40
+ into a single `array` argument: named-argument dispatch then has no key to bind to and breaks.
38
41
  3. **Return** — return `string`; `json_encode()` structured data.
39
42
  4. **`initialize()`** — optional `public static function initialize()` the framework calls
40
43
  automatically before any method in the class (register DB connections / shared setup).
@@ -160,6 +163,9 @@ be reattempted.
160
163
  commit-before-SQS transaction pattern that the worker relies on.
161
164
 
162
165
  ## Change history
166
+ - 2026-06-24 — Clarified that the dispatcher spreads the string-keyed `parameters` as PHP **named
167
+ arguments** (`$class::$method(...$parameters)`), so parameter names are the queue contract — do
168
+ not collapse a multi-param action into a single `array` arg. (mhammontree)
163
169
  - 2026-06-18 — Documented worker-action **exception/retry semantics**: dispatcher catches `Throwable` →
164
170
  `isSuccess=0`+`failureReason`, **always HTTP 200, no DLQ, no auto-retry**; throwing surfaces a failure
165
171
  but does not re-run — retry via `_Worker::runTask` re-enqueue (attempt-counter), in-process loop, or
@@ -0,0 +1,144 @@
1
+ ---
2
+ title: DB-Driven Notification (Internal) Email
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-24
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker2/Worker/Notification/Email.php
13
+ - _underscore/Model/Client/EmailTemplate.php
14
+ - dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql
15
+ - dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql
16
+ related:
17
+ - ../../_underscore/features/email-template-sending.md
18
+ - ./creating-worker-actions.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated,
24
+ not client-facing transactional mail) are sent through one worker action,
25
+ `_Worker_Notification_Email::Send(...)`, which wraps a plain subject/body in the client's
26
+ **DB-stored branded shell** before sending via `_Email`. The branded shell used to be a
27
+ hardcoded PHP class (`_Email_Template`, now **deleted**); it now lives in the client DB as a
28
+ reserved `EmailTemplates` row so the branding can change without a code deploy. This is a
29
+ **shared/core 2.0 mechanism** — every client inherits the wrapper row from the dbchanges2
30
+ `Client/` baseline; it is not specific to any one tenant (`True` is only the pilot tenant the
31
+ migration backfilled first).
32
+
33
+ ## Key files / entry points
34
+
35
+ - `worker2/Worker/Notification/Email.php` — `abstract _Worker_Notification_Email`. One method:
36
+ `Send(string $clientIdentifier, string $subject, string $body, string|array $to=[],
37
+ string|array $cc=[], string|array $bcc=[], string $fromEmail='donotreply@togatech.com',
38
+ string $fromName='TOGA Technology')`. The entry point. Self-registers the client DB (no
39
+ `initialize()` — see below), wraps the body, and sends.
40
+ - `_underscore/Model/Client/EmailTemplate.php` — supplies `WRAPPER_UUID` and
41
+ `renderWrappedBody($subject, $body)`. Documented in
42
+ [`email-template-sending.md`](../../_underscore/features/email-template-sending.md).
43
+ - `dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql` — blank-client baseline; seeds the
44
+ wrapper row so **every new client** inherits it.
45
+ - `dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql` — backfills the pilot tenant.
46
+
47
+ ## How it works
48
+
49
+ 1. **Validate the identifier.** `$clientIdentifier` is interpolated into a schema name
50
+ (`Client_<id>`), so `Send()` first rejects anything that isn't `^[A-Za-z0-9]+$` (matches
51
+ `Core.Clients.clientIdentifier`, e.g. `True`, `CompassCanada`). This is the injection guard —
52
+ keep it.
53
+ 2. **Register the client DB under the `DB_CLIENT` alias** via
54
+ `_Database::register('Client_'.$id, _Config::databaseClient(...), …, _underscore::DB_CLIENT)`.
55
+ This points the process-global `Client` alias at the concrete `Client_<id>` schema so the
56
+ `DATABASE = DB_CLIENT` model resolves to the right tenant. Only the `Client` alias is
57
+ registered — **not** `DB_CLIENT_LOGS` (see gotchas).
58
+ 3. **Wrap the body.** `_Model_Client_EmailTemplate::renderWrappedBody($subject, $body)` loads the
59
+ active wrapper row (uuid = `WRAPPER_UUID`) and `strtr()`-substitutes `{subject}`/`{body}`.
60
+ Returns `null` if there is no active wrapper row.
61
+ 4. **Graceful degrade.** If `renderWrappedBody` returns `null`, `Send()` `error_log()`s the
62
+ missing wrapper (with the uuid + client) and sends the **raw, unwrapped** body. These are
63
+ internal alerts — delivering unbranded mail beats dropping the alert. (Revisit if this path is
64
+ ever reused for client-facing email, where branding should be guaranteed.)
65
+ 5. **Send** via a plain `_Email`: `setClientIdentifier`, `addTo/Cc/Bcc`, `setFrom`,
66
+ `setSubject`, `setBody($wrapped ?? $raw)`, `send()`.
67
+
68
+ ### Enqueuing it
69
+
70
+ `_Worker::runTask('Notification/Email/Send', ['clientIdentifier'=>…, 'subject'=>…, 'body'=>…, …])`
71
+ inserts a WorkerJob. The dispatcher (`worker2/Controller/Index.php`) resolves
72
+ `'Notification/Email/Send'` → `_Worker_Notification_Email::Send`, then spreads the **string-keyed**
73
+ parameters array as PHP **named arguments**. So the method's named parameters *are* the queue's
74
+ parameter contract — see [`creating-worker-actions.md`](./creating-worker-actions.md).
75
+
76
+ **Proven caller:** `_Worker_Team_GitHub::Merge()` (`worker2/Worker/Team/Github.php`) enqueues
77
+ `Notification/Email/Send` on a git merge conflict, so merge-conflict alerts now go out branded.
78
+
79
+ ## The wrapper row (data model)
80
+
81
+ A single reserved `EmailTemplates` row per client DB:
82
+
83
+ - **uuid** = `_Model_Client_EmailTemplate::WRAPPER_UUID` (`11111111-1111-4111-8111-111111111111`).
84
+ This literal is the contract between the model lookup and the seed SQL — both must use it.
85
+ - **body** holds the full branded HTML with `{subject}` and `{body}` placeholders.
86
+ - Seeded once in `dbchanges2/Client/…` (baseline, so new clients inherit it) and backfilled per
87
+ existing client (`dbchanges2/Client_True/…` did the pilot). In the seed SQL the CSS font names
88
+ are left **unquoted** so the `INSERT` has no single quotes to escape.
89
+
90
+ ## Branded header — Outlook bulletproofing (load-bearing recipe)
91
+
92
+ The wrapper's header must render the TOGA line-art logo
93
+ (`TOGA-TECHNOLOGY-Header-Logo-Vector.png`, sized **exactly 600×69** = the header box) in
94
+ **both** classic Outlook and new Outlook/Gmail/Apple, **without** the logo rendering twice. The
95
+ working recipe — verified in classic + new Outlook:
96
+
97
+ - **Classic Outlook:** VML — `<v:rect>` with `<v:fill type="frame" src=…>` and a `<v:textbox>`
98
+ holding the title.
99
+ - **New Outlook / Gmail / Apple Mail:** the HTML `background=` **attribute** on the cell.
100
+ - **No CSS `background-image`** on the header — only `background-color`. New Outlook would
101
+ otherwise render the CSS image *and* the `background=` attribute = the double-render.
102
+ - **Horizontal inset belongs on the title `<div>`** (`padding: 22px 90px`), **not** on the `<td>`.
103
+ Putting padding on the `<td>` squeezes the VML/background image inward in classic Outlook so it
104
+ no longer fills the full header width.
105
+ - The **footer** logo is a normal `<img>` (no VML needed).
106
+
107
+ ## Gotchas / known issues
108
+
109
+ - **No `initialize()` on this action.** `_Worker_Notification_Email` does not define
110
+ `initialize()`, so the dispatcher never calls one — `Send()` registers its own client DB inline.
111
+ (The dispatcher only calls `initialize()` when the class defines it.)
112
+ - **A `DATABASE = DB_CLIENT` model resolves only after the `Client` alias is registered** to the
113
+ concrete `Client_<x>` schema. `renderWrappedBody()` will load the wrong (or no) tenant if you
114
+ call it before `_Database::register(..., _underscore::DB_CLIENT)`. Authoritative pattern:
115
+ `_Worker_Ai_Bdr_Netsuite::initialize()`.
116
+ - **Do not register `DB_CLIENT_LOGS` for this path.** `_Email` logs to CloudWatch, not a Logs DB,
117
+ so the notification path never needs it — and `[databaseLogs]` only exists in `production.ini`,
118
+ so registering it in dev/beta throws.
119
+ - **Debug mode redirects, doesn't suppress.** When `_Email::isDebugMode()` is on, `_Email::send()`
120
+ redirects the recipient to `[_underscore] send_debug_emails_to` and prepends a
121
+ "DEVELOPMENT MODE - Intended Recipients" banner — the email still sends.
122
+ - **Missing wrapper row → unbranded send, logged.** If a client has no active wrapper row the alert
123
+ still goes out (raw body) and an `error_log` line records the missing uuid. Branding is *not*
124
+ guaranteed on this path by design (internal mail). Don't rely on it for client-facing email.
125
+ - **`{subject}`/`{body}` substitution is `strtr`, not sequential `str_replace`.** All placeholders
126
+ are replaced simultaneously, so a `{body}` literal inside the subject can't be re-expanded.
127
+
128
+ ## Change history
129
+ - 2026-06-24 — Built the DB-driven notification-email mechanism (TRUE-79240): new
130
+ `_Worker_Notification_Email::Send` entry point; branded shell moved out of code (deleted
131
+ `_underscore/Email/Template.php`, which also carried the 1.0 double-render bug) into a reserved
132
+ per-client `EmailTemplates` wrapper row seeded by dbchanges2 (`Client/` baseline +
133
+ `Client_True/` backfill). Missing wrapper degrades to an unbranded send (logged). Documented the
134
+ Outlook-bulletproof header recipe (VML + `background=` attribute, no CSS background-image, inset
135
+ on the title `<div>`). Merge-conflict alerts (`_Worker_Team_GitHub::Merge`) now use this path.
136
+ (mhammontree)
137
+
138
+ ## Related docs
139
+ - [`2.0/apps/_underscore/features/email-template-sending.md`](../../_underscore/features/email-template-sending.md)
140
+ — the `_Model_Client_EmailTemplate` model, including `WRAPPER_UUID` + `renderWrappedBody()`.
141
+ - [`creating-worker-actions.md`](./creating-worker-actions.md) — worker action dispatch +
142
+ named-argument parameter contract.
143
+ - `1.0/apps/library/features/email-templates.md` — the **1.0** `App_Email_Template` (a different
144
+ framework/mechanism; cross-link only, do not conflate).
@@ -16,12 +16,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
16
16
  ## 2.0 framework
17
17
 
18
18
  - **_underscore** (_Underscore) _(framework core)_ — 12 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
- - **worker2** (Worker) — 13 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
19
+ - **worker2** (Worker) — 14 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
20
  - **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
21
  - **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
22
22
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
23
23
  - **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
24
- - **toga2-view** (TOGa View Frontend) — 2 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
24
+ - **toga2-view** (TOGa View Frontend) — 4 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
25
25
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
26
26
  - **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
27
27
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
@@ -10,8 +10,8 @@ project: SAML SSO Gateway
10
10
  client: rate
11
11
  type: profile
12
12
  status: active
13
- updated: 2026-06-16
14
- owners: ["rgirish"]
13
+ updated: 2026-06-24
14
+ owners: ["rgirish", "bala"]
15
15
  files: []
16
16
  related:
17
17
  - clients/rate/features/saml-sso.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.192",
3
+ "version": "1.0.194",
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",