toga-ai 1.0.333 → 1.0.335
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +39 -0
- package/knowledge/2.0/apps/_underscore/features/cloud-s3-helpers.md +17 -4
- package/knowledge/2.0/apps/worker2/INDEX.md +2 -1
- package/knowledge/2.0/apps/worker2/features/clickup-project-routing.md +7 -2
- package/knowledge/2.0/apps/worker2/features/clickup-subtask-activity.md +117 -0
- package/knowledge/2.0/apps/worker2/features/oneuptime-worker2-monitoring.md +128 -59
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
7
7
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
8
8
|
| [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
|
|
9
|
+
| [_ApiRequest ENCODE__JSON now sends Content-Type: application/json](features/apirequest-json-content-type.md) | `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set a `Content-Type` header. | _underscore/ApiRequest.php |
|
|
9
10
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
10
11
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
11
12
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: _ApiRequest ENCODE__JSON now sends Content-Type: application/json
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-14
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/ApiRequest.php
|
|
13
|
+
related:
|
|
14
|
+
- ../../worker2/features/oneuptime-worker2-monitoring.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
`_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set
|
|
19
|
+
a `Content-Type` header. cURL therefore defaulted to `application/x-www-form-urlencoded`, and
|
|
20
|
+
receivers parsed the entire JSON string as a single form-field NAME — observed at OneUptime as
|
|
21
|
+
`{"<json>":""}`. Every 2.0 `ENCODE__JSON` POST/PUT/PATCH caller was silently shipping
|
|
22
|
+
form-urlencoded bodies.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
The `ENCODE__JSON` branch now adds `Content-Type: application/json`. It is guarded by a
|
|
26
|
+
case-insensitive scan of already-set headers, so a caller-supplied `Content-Type` still wins
|
|
27
|
+
(no override). All other encodings are unaffected.
|
|
28
|
+
|
|
29
|
+
## Gotchas / known issues
|
|
30
|
+
- This changes the wire format of ALL existing `ENCODE__JSON` callers (they were previously
|
|
31
|
+
sending form-urlencoded). This is a correction, but smoke-test heavy JSON callers post-deploy:
|
|
32
|
+
ClickUp, NetSuite, Vapi.
|
|
33
|
+
|
|
34
|
+
## Change history
|
|
35
|
+
- 2026-07-14 — Fixed `ENCODE__JSON` to send `Content-Type: application/json` (guarded so a
|
|
36
|
+
caller-set Content-Type wins). Root-caused via OneUptime receiving `{"<json>":""}`. Affects
|
|
37
|
+
all 2.0 ENCODE__JSON callers. (jcardinal)
|
|
38
|
+
</content>
|
|
39
|
+
</invoke>
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Cloud.php
|
|
@@ -22,7 +22,7 @@ workers or app code. Alongside the existing copy / get / delete methods it now h
|
|
|
22
22
|
|
|
23
23
|
## Key files / entry points
|
|
24
24
|
|
|
25
|
-
- `_underscore/Cloud.php` — `_Cloud::getS3Objects($s3BucketName, $s3BucketPath = '')`.
|
|
25
|
+
- `_underscore/Cloud.php` — `_Cloud::getS3Objects($s3BucketName, $s3BucketPath = '', ?string $awsRegion = null)`.
|
|
26
26
|
|
|
27
27
|
## How it works
|
|
28
28
|
|
|
@@ -33,10 +33,18 @@ workers or app code. Alongside the existing copy / get / delete methods it now h
|
|
|
33
33
|
- `LastModified` — ISO-8601 string
|
|
34
34
|
- `Size`
|
|
35
35
|
|
|
36
|
-
The method is **
|
|
37
|
-
|
|
36
|
+
The method is **additive** — the optional 3rd param `?string $awsRegion = null` falls back
|
|
37
|
+
to `_Config::cloud('aws_region')`, so existing 2-arg callers are unchanged. Keeping S3
|
|
38
|
+
listing here (rather than instantiating `S3Client` in a worker) is the reason to prefer
|
|
38
39
|
this helper over ad-hoc SDK calls.
|
|
39
40
|
|
|
41
|
+
### Per-bucket region override
|
|
42
|
+
|
|
43
|
+
The config-default region is **us-east-1**. Buckets in another region must pass
|
|
44
|
+
`$awsRegion` explicitly or the SDK throws `AuthorizationHeaderMalformed`. Notably the
|
|
45
|
+
**`agilant-as2`** bucket lives in **us-west-2** — the worker2 Office Depot S3 monitors pass
|
|
46
|
+
`'us-west-2'`.
|
|
47
|
+
|
|
40
48
|
First consumer: the worker2 Office Depot EDI backlog monitor — see
|
|
41
49
|
[OneUptime push-metric monitors](../../worker2/features/oneuptime-worker2-monitoring.md).
|
|
42
50
|
|
|
@@ -48,7 +56,12 @@ None — shared core helper.
|
|
|
48
56
|
|
|
49
57
|
- Callers get every matching key regardless of count; pagination is handled internally, so
|
|
50
58
|
do not add your own `ContinuationToken` loop on top.
|
|
59
|
+
- A bucket outside the config-default us-east-1 (e.g. `agilant-as2` in us-west-2) throws
|
|
60
|
+
`AuthorizationHeaderMalformed` unless you pass `$awsRegion`.
|
|
51
61
|
|
|
52
62
|
## Change history
|
|
63
|
+
- 2026-07-14 — `getS3Objects()` gained an optional `?string $awsRegion = null` 3rd param
|
|
64
|
+
(falls back to `_Config::cloud('aws_region')`); needed because `agilant-as2` is in
|
|
65
|
+
us-west-2, not the config-default us-east-1. Additive — 2-arg callers unchanged. (jcardinal)
|
|
53
66
|
- 2026-07-13 — Added `_Cloud::getS3Objects()` S3 LIST helper (paginated `listObjectsV2`,
|
|
54
67
|
returns `{Key, LastModified, Size}`); additive, no existing signatures changed. (jcardinal)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [ClickUp GitHub-tab Auto-linking & Ticket-id Branch Naming](features/clickup-github-autolink.md) | How ClickUp surfaces branches/PRs/commits in a ticket's **GitHub tab**, and the branch / PR-title naming convention that triggers it. | |
|
|
10
10
|
| [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into the correct **secondary multi-list memberships** based on their custom-field values, via the `clickup` webhook. | worker2/Worker/Clickup/Project.php, worker2/Worker/Clickup.php |
|
|
11
11
|
| [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | ClickUp custom text fields (type `text` and long-text) support rich formatting only through a **Quill Delta** written to the undocumented `value_richtext` key o | test/@dave/clickup_md2delta.js, .claude/skills/plan-ticket/scripts/clickup.js |
|
|
12
|
+
| [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity** or **Epic** it rolls up to, so a deal/project owner sees activity whe | worker2/Worker/Clickup/Subtask.php, worker2/Worker/Clickup.php, dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql |
|
|
12
13
|
| [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
|
|
13
14
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
14
15
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
|
|
21
22
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
22
23
|
| [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 |
|
|
23
|
-
| [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, _underscore/Cloud.php |
|
|
24
|
+
| [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php |
|
|
24
25
|
| [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 |
|
|
25
26
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
26
27
|
| [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
|
|
@@ -15,6 +15,7 @@ related:
|
|
|
15
15
|
- ../architecture.md
|
|
16
16
|
- ./creating-worker-actions.md
|
|
17
17
|
- ./clickup-connectivity-watchdog.md
|
|
18
|
+
- ./clickup-subtask-activity.md
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
## Summary
|
|
@@ -78,8 +79,11 @@ ids are discovered. Configure via the `DiscoverTaskTypes()` / `DiscoverIds()` ac
|
|
|
78
79
|
2. Each handler fetches task details via `_Worker_Clickup::getTaskDetails()` (a per-invocation
|
|
79
80
|
`static` cache shared across all handlers in one job), then guards on **space id** and
|
|
80
81
|
**top-level** (`parent` empty). The three routing handlers identify Epics/tasks
|
|
81
|
-
**structurally** (space + top-level).
|
|
82
|
-
|
|
82
|
+
**structurally** (space + top-level). The `enforceHubPlacement` guard was written to key on
|
|
83
|
+
`custom_item_id`, but the Task-Types ClickApp is **not reliably enabled** on the hub spaces
|
|
84
|
+
and `TASK_TYPE_EPIC_ID` / `TASK_TYPE_OPPORTUNITY_ID` remain `null` — so that guard is
|
|
85
|
+
effectively **inert** in practice; structural (space + top-level) identification is what
|
|
86
|
+
actually runs (see Gotchas, and `./clickup-subtask-activity.md`).
|
|
83
87
|
3. Custom fields are read by name via `extractCustomFields()` (one pass, last-non-null-wins,
|
|
84
88
|
trimmed) → mapped to a target list id → reconciled by `applyMultiListPlacement()`:
|
|
85
89
|
POST the target if absent, DELETE every other in-scope list (DELETE failures are caught
|
|
@@ -160,6 +164,7 @@ Read-only / setup actions, invoked via `curl -X POST https://worker.togahub.com/
|
|
|
160
164
|
`RESULT: PASS`), `RegisterOpportunityWebhook` and `RegisterHubGuardWebhooks` (one-time setup).
|
|
161
165
|
|
|
162
166
|
## Change history
|
|
167
|
+
- 2026-07-14 — Correction: `TASK_TYPE_EPIC_ID` / `TASK_TYPE_OPPORTUNITY_ID` are still `null` and the Task-Types ClickApp is not reliably enabled on the hub spaces, so `enforceHubPlacement`'s custom-Task-Type keying is inert; structural (space + top-level) identification is what actually runs. Cross-linked the new subtask-activity feature (same hub spaces/pipeline). (ajean)
|
|
163
168
|
- 2026-06-23 — Added the hub-placement guard (`enforceHubPlacement` on `taskCreated`/`taskMoved`): re-anchors misplaced Epics/Opportunities to their hub by custom Task Type + home space, backfills Business Unit/Project Category, and notifies. Added `DiscoverTaskTypes`, `RegisterHubGuardWebhooks`, `TestHubGuardMappings`. Corrected the earlier "Task Types not enabled" note. (ajean)
|
|
164
169
|
- 2026-06-09 — Documented ClickUp secondary multi-list routing (hybrid native-automation + worker design, three space handlers, self-trigger guard). (jcardinal)
|
|
165
170
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ClickUp Subtask Activity → Parent Opportunity/Epic Comments
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-07-14
|
|
10
|
+
owners: [ajean]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Clickup/Subtask.php
|
|
13
|
+
- worker2/Worker/Clickup.php
|
|
14
|
+
- dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql
|
|
15
|
+
related:
|
|
16
|
+
- ./clickup-project-routing.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity**
|
|
23
|
+
or **Epic** it rolls up to, so a deal/project owner sees activity where they actually look.
|
|
24
|
+
On a `clickup` webhook for a subtask (a task with a non-empty `parent`) of event
|
|
25
|
+
`taskStatusUpdated` or `taskCommentPosted`, the worker walks the parent chain to the
|
|
26
|
+
top-level ancestor and, if that ancestor is an Opportunity (Opportunity/RFP Hub space
|
|
27
|
+
`90113928591`) or an Epic (Project Hub space `90113939341`), posts **one** comment on the
|
|
28
|
+
ancestor. Implemented in `_Worker_Clickup_Subtask` (extends `_Worker_Clickup`), fanned out
|
|
29
|
+
from a pre-switch block in `_Worker_Clickup::Webhook()`.
|
|
30
|
+
|
|
31
|
+
> **Status: not yet deployed / not live-verified.** PRs open: worker2 #110, dbchanges2 #427.
|
|
32
|
+
> The dbchanges2 migration must run **before or with** the worker deploy (the worker reserves
|
|
33
|
+
> a ledger row before every post).
|
|
34
|
+
|
|
35
|
+
## Key files / entry points
|
|
36
|
+
|
|
37
|
+
- `worker2/Worker/Clickup/Subtask.php` — `_Worker_Clickup_Subtask` (NEW), all subtask
|
|
38
|
+
fan-out / ancestor-walk / dedup / post logic.
|
|
39
|
+
- `worker2/Worker/Clickup.php` — a **pre-switch fan-out block** in `_Worker_Clickup::Webhook()`
|
|
40
|
+
detects a subtask event and dispatches `_Worker::runTask('Clickup/Subtask/Process',
|
|
41
|
+
['taskId', 'event', 'data' => history_items])`. The block is wrapped in a best-effort
|
|
42
|
+
`try/catch (Throwable)` and **deliberately does not `return`** — it falls through to the
|
|
43
|
+
existing `taskStatusUpdated` / `taskCommentPosted` switch, so sprint / work-type handling is
|
|
44
|
+
unaffected.
|
|
45
|
+
- `dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql` (NEW) — creates the
|
|
46
|
+
`ClickUpSubtaskActivity` dedup ledger table in the **Team** DB.
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
1. Webhook arrives → `_Worker_Clickup::Webhook()`. The pre-switch block short-circuits **early
|
|
51
|
+
on the subtask's own space** before any parent walk (a subtask lives in the same space as its
|
|
52
|
+
ancestor, so a non-hub space is discarded before spending API calls).
|
|
53
|
+
2. `Clickup/Subtask/Process` walks up the `parent` chain to the top-level (no-parent) ancestor
|
|
54
|
+
and confirms it is an Opportunity or Epic **structurally** — by home space id + top-level —
|
|
55
|
+
mirroring `handleEpicUpdate` / `handleOpportunityUpdate`. **Not** by custom Task Type: the
|
|
56
|
+
Task-Types ClickApp is not reliably enabled on the hub spaces (see Gotchas).
|
|
57
|
+
3. Triggers:
|
|
58
|
+
- **Status** — post when the subtask status moves into `in progress`, `roadblocked`, or
|
|
59
|
+
`on hold` (matched by **lowercase status name**, consistent with existing status
|
|
60
|
+
comparisons in `Worker/Clickup.php`), OR into any completed status (matched by **status
|
|
61
|
+
TYPE** in `done`/`closed` via the inherited `COMPLETE_STATUS_TYPES`).
|
|
62
|
+
- **Comment** — any user comment posted on the subtask is mirrored to the ancestor as
|
|
63
|
+
`"💬 <author> commented on subtask …"`.
|
|
64
|
+
4. Every post carries a trailing `(automated)` marker. Comments that already carry that marker
|
|
65
|
+
are **never** mirrored (loop guard).
|
|
66
|
+
5. Posts use `_Component_Api_Clickup::send('POST', '/task/{id}/comment',
|
|
67
|
+
['comment_text' => …, 'notify_all' => false])`; task lookups reuse
|
|
68
|
+
`_Worker_Clickup::getTaskDetails()` (in-process static cache);
|
|
69
|
+
`_Worker_Clickup_Project::RATE_LIMIT_DELAY_US` paces the parent walk.
|
|
70
|
+
|
|
71
|
+
### Dedup ledger (the authoritative guard)
|
|
72
|
+
|
|
73
|
+
Before posting, the worker **reserves a ledger row** (`INSERT` into `ClickUpSubtaskActivity`)
|
|
74
|
+
so the `UNIQUE(subtaskId, activityKey)` constraint — not a check-then-act window — is the
|
|
75
|
+
authoritative dedup guard against re-delivered webhooks. If the comment POST fails, the row is
|
|
76
|
+
**released** (`DELETE`) so a retry can re-attempt. The `activityKey` is:
|
|
77
|
+
- `status:<name>` for a watched named status,
|
|
78
|
+
- `status:completed` for a completion,
|
|
79
|
+
- `comment:<clickupCommentId>` for a mirrored comment — falling back to the history-item id,
|
|
80
|
+
then a per-event date, then `md5(text)`, so identical comment texts stay distinct.
|
|
81
|
+
|
|
82
|
+
Ledger access follows the `ClickUpHealthState` pattern from `_Worker_Clickup_Health`: raw
|
|
83
|
+
`_Query` on `_underscore::DB_TEAM` with `_Database::escape()`.
|
|
84
|
+
|
|
85
|
+
## Data model
|
|
86
|
+
|
|
87
|
+
`ClickUpSubtaskActivity` in the **Team** DB (dbchanges2 migration `2026-07-14a`):
|
|
88
|
+
`id`, `uuid`, `subtaskId VARCHAR(32)`, `activityKey VARCHAR(255)`, `dtPosted`, with
|
|
89
|
+
`UNIQUE(subtaskId, activityKey)`. All other state lives in ClickUp.
|
|
90
|
+
|
|
91
|
+
## Client variations
|
|
92
|
+
|
|
93
|
+
None — internal automation for Agilant's ClickUp workspace, uniform across clients.
|
|
94
|
+
|
|
95
|
+
## Gotchas / known issues
|
|
96
|
+
|
|
97
|
+
- **Ancestor identification is STRUCTURAL, not by Task Type.** The Task-Types ClickApp is not
|
|
98
|
+
reliably enabled on the hub spaces, and `_Worker_Clickup_Project`'s `TASK_TYPE_EPIC_ID` /
|
|
99
|
+
`TASK_TYPE_OPPORTUNITY_ID` remain `null` (inert). Identify Opportunities/Epics by home space
|
|
100
|
+
id + top-level, exactly like the routing handlers — do not rely on `custom_item_id` here.
|
|
101
|
+
- **Completion matches by status TYPE (`done`/`closed`), not name** — it survives label
|
|
102
|
+
changes. The other watched statuses match by lowercase **name** (`in progress`,
|
|
103
|
+
`roadblocked`, `on hold`).
|
|
104
|
+
- **Loop guard is the `(automated)` marker** — every automated post carries it, and any comment
|
|
105
|
+
carrying it is skipped on mirror. Removing/altering the marker text re-opens the mirror loop.
|
|
106
|
+
- **Fan-out never returns** — it falls through to the existing switch on purpose; keep it that
|
|
107
|
+
way or existing sprint/work-type handling breaks.
|
|
108
|
+
- **Migration ordering** — deploy the dbchanges2 `2026-07-14a` migration before/with the worker;
|
|
109
|
+
the worker reserves a ledger row on every post and will error without the table.
|
|
110
|
+
|
|
111
|
+
## Change history
|
|
112
|
+
- 2026-07-14 — Created (draft, not yet deployed): subtask status/completion/comment mirroring to the parent Opportunity/Epic via `_Worker_Clickup_Subtask`, guarded by the `ClickUpSubtaskActivity` UNIQUE-key dedup ledger. PRs worker2 #110 / dbchanges2 #427. (ajean)
|
|
113
|
+
|
|
114
|
+
## Related docs
|
|
115
|
+
|
|
116
|
+
- [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md) — same hub spaces, same webhook pipeline, same structural identification.
|
|
117
|
+
- [Worker (worker2) Architecture](../architecture.md) — always-HTTP-200, commit-before-SQS worker contract.
|
|
@@ -6,10 +6,12 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-14
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Compass.php
|
|
13
|
+
- worker2/Worker/Client/Compass.php
|
|
14
|
+
- worker2/composer.json
|
|
13
15
|
- _underscore/Cloud.php
|
|
14
16
|
related:
|
|
15
17
|
- ./monitoring-framework.md
|
|
@@ -22,10 +24,10 @@ related:
|
|
|
22
24
|
A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from
|
|
23
25
|
the 1.0 `App_SystemMonitor_Compass` monitors. A worker2 cron action runs a self-contained
|
|
24
26
|
check, decides pass/fail itself, and POSTs a JSON metric body to a OneUptime "Incoming
|
|
25
|
-
Request" monitor — replacing the 1.0 tier's email alerts.
|
|
26
|
-
`_Worker_Monitor_Compass
|
|
27
|
-
|
|
28
|
-
future monitors and clients.
|
|
27
|
+
Request" monitor — replacing the 1.0 tier's email alerts. The class is
|
|
28
|
+
`abstract class _Worker_Monitor_Compass` and now carries a **suite of 9 monitors** covering
|
|
29
|
+
the Compass USA / Office Depot integration (S3, DB, and mailbox checks). Designed to be
|
|
30
|
+
**client-agnostic** — a reusable template for future monitors and clients.
|
|
29
31
|
|
|
30
32
|
This is distinct from the DB-driven email-orchestrator
|
|
31
33
|
[Monitoring Framework](./monitoring-framework.md) (`_Worker_Monitor` orchestrator +
|
|
@@ -37,92 +39,159 @@ that reports straight to OneUptime. Note the folder difference: these live under
|
|
|
37
39
|
## Key files / entry points
|
|
38
40
|
|
|
39
41
|
- `worker2/Worker/Monitor/Compass.php` — `abstract class _Worker_Monitor_Compass`; each
|
|
40
|
-
monitor is a `public static` action method
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
|
|
42
|
+
monitor is a `public static` action method dispatched via a `Core.CronJobs` action
|
|
43
|
+
`Monitor/Compass/<Method>` and returning a summary string recorded in `WorkerJobs`
|
|
44
|
+
(standard worker2 action conventions). `initialize()` registers the Compass client DB by
|
|
45
|
+
delegating to `_Worker_Client_Compass::initialize()` (not per-method).
|
|
46
|
+
- `worker2/Worker/Client/Compass.php` — `_Worker_Client_Compass`; owns the Compass id
|
|
47
|
+
constants and the `DB_CLIENT_COMPASS` connection name the monitors query against.
|
|
48
|
+
- `_underscore/Cloud.php` — `_Cloud::getS3Objects()` list helper the S3 monitors rely on
|
|
44
49
|
(see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
|
|
45
50
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
## The 9 monitors
|
|
52
|
+
|
|
53
|
+
Each measures one Compass/Office Depot integration metric and pushes a decided alarm token.
|
|
54
|
+
- **OfficeDepotEdiImportQueue** (S3) — counts files >10 min old in
|
|
55
|
+
`s3://agilant-as2/OfficeDepot/`, excluding `OUTBOX/`, `SENT/` and directory placeholders.
|
|
56
|
+
- **CompassSalesOrderTransmission** (DB) — approved SOs (both approval stages) with
|
|
57
|
+
`c_dtTransmittedToCompass` NULL (not yet transmitted to MITS).
|
|
58
|
+
- **CompassIncomingPurchaseOrders** (DB) — `SA%` SOs transmitted to MITS >4h ago (on/after
|
|
59
|
+
the 2026-04-30 cutoff) with no PO returned.
|
|
60
|
+
- **CompassPurchaseOrderTransmissionsToVendors** (DB) — unsubmitted POs for monitored
|
|
61
|
+
vendors (Office Depot, Strategic Systems, Compass, Presidio) due today or earlier.
|
|
62
|
+
- **EmailedAsnImport** (mailbox) — counts inbox messages in the emailed-ASN mailbox via
|
|
63
|
+
IMAP XOAUTH2 (see mailbox decision below).
|
|
64
|
+
- **OfficeDepotPoAcknowledgements** (DB) — Office Depot POs with `dtAcknowledged` NULL
|
|
65
|
+
through the OD→Compass PO/SO chain.
|
|
66
|
+
- **OfficeDepotNetsuiteIntegration** (DB) — OD SOs not in NetSuite
|
|
67
|
+
(`c_dtTransmittedToNetsuite` NULL) with a 30-min PO-grace subquery.
|
|
68
|
+
- **OfficeDepotAsnExport** (DB) — OD orders whose ASN not sent (ItemFulfillments with
|
|
69
|
+
`c_dtEdi856Sent` NULL + a tracking number, on/after the 2025-02-04 cutover). Covers all
|
|
70
|
+
order types (SA/MR/MA) via ItemFulfillments/tracking-number logic. (The 1.0 monitor
|
|
71
|
+
TRUE-75101 in `library/app/systemmonitor/compass.php` was intentionally NOT ported — its
|
|
72
|
+
logic is superseded by this corrected definition.)
|
|
73
|
+
- **OfficeDepotAs2Outbox** (S3) — counts ONLY top-level files in
|
|
74
|
+
`s3://agilant-as2/OfficeDepot/OUTBOX/` (skips subfolders + directory placeholders).
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
older than `AGED_FILE_MINUTES` (10), **excluding** the `OUTBOX/` and `SENT/` sub-prefixes
|
|
53
|
-
and directory-placeholder keys.
|
|
54
|
-
2. Decides the alarm state itself and POSTs a JSON metric body to the OneUptime "Incoming
|
|
55
|
-
Request" monitor via `_ApiRequest` — with **logging disabled** and
|
|
56
|
-
**`throwExceptionsOnFailure` disabled**, so a failed ping never fails the worker job.
|
|
57
|
-
3. On S3 failure it still POSTs `{"status":"error"}` so a blind/dead checker is
|
|
58
|
-
distinguishable from a real backlog.
|
|
76
|
+
## How it works
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
Each monitor is a self-contained method: per-monitor config as `ALL_CAPS` local variables
|
|
79
|
+
(PHP disallows `const` at function scope), plus a `$push` closure using `_ApiRequest` — all
|
|
80
|
+
inside the method, so the class scales cleanly as monitors accumulate. The metric POST runs
|
|
81
|
+
with **logging disabled** and **`throwExceptionsOnFailure` disabled**, so a failed ping
|
|
82
|
+
never fails the worker job.
|
|
62
83
|
|
|
63
|
-
###
|
|
84
|
+
### Payload contract & OneUptime criteria (critical — OneUptime cannot compare numbers)
|
|
64
85
|
|
|
65
86
|
OneUptime **Incoming Request** monitors **cannot** do numeric threshold comparison on a
|
|
66
|
-
pushed body
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
**not** support Greater Than / Less Than (those filter types apply only to
|
|
70
|
-
outgoing/synthetic checks like Response Time / Status Code / Metric Value).
|
|
71
|
-
|
|
72
|
-
Consequence: the "dumb reporter, smart monitor" ideal (push a raw number, let OneUptime
|
|
73
|
-
compare `> threshold`) is **not achievable for push monitors**. Instead the **worker makes
|
|
74
|
-
the threshold decision** and emits a string token that OneUptime matches with `Contains`.
|
|
75
|
-
OneUptime criteria for this monitor:
|
|
87
|
+
pushed body (`checkOn: "Request Body"` supports only `Contains` / `NotContains` **string**
|
|
88
|
+
matching — no JSON parse, no nested-key targeting, no Greater/Less Than). So the **worker
|
|
89
|
+
makes the threshold decision** and emits a string token OneUptime matches with `Contains`.
|
|
76
90
|
|
|
91
|
+
Payload body: `{status:"reporting"|"error", alarm:"HIGH"|"OK", <metric fields>, checkedAtUtc}`.
|
|
92
|
+
OneUptime criteria:
|
|
77
93
|
- Request Body **Contains** `"alarm":"HIGH"` → Offline + incident.
|
|
78
|
-
- Request Body **Contains** `"status":"error"` → Offline + incident
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
- Request Body **Contains** `"status":"error"` → Offline + incident (distinguishes a
|
|
95
|
+
blind/dead checker from a real backlog).
|
|
96
|
+
- **not received in 10 min** → Degraded.
|
|
97
|
+
- **received in 1 min AND Contains `"alarm":"OK"`** → Online (the OK-gated recovery
|
|
98
|
+
prevents flapping back to Operational while the alarm is still HIGH).
|
|
99
|
+
- **not received in 15 min** → Offline.
|
|
81
100
|
|
|
82
101
|
### Heartbeat / cron cadence timing
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
cadence
|
|
86
|
-
false-alarmed on a single missed ping. The Office Depot backlog alarm threshold is
|
|
87
|
-
**10 aged files**.
|
|
103
|
+
The 10-min-Degraded / 15-min-Offline heartbeat thresholds pair with a **5-minute** cron
|
|
104
|
+
cadence. An initial 3/5-min setting false-alarmed on a single missed ping.
|
|
88
105
|
|
|
89
106
|
## Provisioning a monitor (runbook)
|
|
90
107
|
|
|
91
|
-
1. Write the monitor method on `_Worker_Monitor_Compass` (or a new `_Worker_Monitor_<X>`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`const` at function scope), so the class stays clean as monitors accumulate.
|
|
95
|
-
2. Provision the OneUptime monitor by cloning the reusable import/export template at
|
|
96
|
-
`monitor-import-OfficeDepot-EDI-1.0.json` (a local dev artifact, not a repo file) and
|
|
97
|
-
importing it into OneUptime; wire the Contains criteria above.
|
|
98
|
-
3. Register the OneUptime push URL as a local/constant in the monitor method — it is a
|
|
108
|
+
1. Write the monitor method on `_Worker_Monitor_Compass` (or a new `_Worker_Monitor_<X>`),
|
|
109
|
+
fully self-contained per above.
|
|
110
|
+
2. Set the OneUptime heartbeat URL as an in-file constant/local in the method — it is a
|
|
99
111
|
**push credential**; never log it and never record its value in a doc.
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
3. Provision the OneUptime monitor by importing the monitor's OneUptime resource-export
|
|
113
|
+
JSON (authored under a local staging dir, e.g. `d:\STAGE` — these are import artifacts,
|
|
114
|
+
not repo files) and wiring the Contains criteria above.
|
|
115
|
+
4. Add the `Core.CronJobs` row: `action = 'Monitor/Compass/<Method>'`, schedule
|
|
116
|
+
`*/5 * * * *`, `maxExecutionTime` 120s, `parameters` NULL. Then deploy worker2.
|
|
117
|
+
|
|
118
|
+
### CronJob scheduling for these monitors
|
|
119
|
+
|
|
120
|
+
Run **all hours**, `*/5 * * * *` (every 5 min) — **not** the 1.0 `6-20` business-hours
|
|
121
|
+
window. Integration backlogs must be caught outside business hours too.
|
|
122
|
+
|
|
123
|
+
## DB monitor conventions
|
|
124
|
+
|
|
125
|
+
- Query via `new _Query($sql, _Worker_Client_Compass::DB_CLIENT_COMPASS)`.
|
|
126
|
+
- `_Query::fetchRow()` returns an **OBJECT** (`$row->pendingCount`), not an array.
|
|
127
|
+
- The client DB connection is registered by the class `initialize()` hook (delegating to
|
|
128
|
+
`_Worker_Client_Compass::initialize()`), not per method.
|
|
129
|
+
- All Compass id constants live on **`_Worker_Client_Compass`** (VENDOR_ID__*,
|
|
130
|
+
CUSTOMER_ID__*, UUID_*, DB_CLIENT_COMPASS) — moved out of the 1.0 `App_Client_Compass`.
|
|
131
|
+
Never reference `App_Client_Compass` from worker2 (see gotcha below).
|
|
132
|
+
|
|
133
|
+
## Mailbox monitoring (EmailedAsnImport) — IMAP XOAUTH2, not Graph
|
|
134
|
+
|
|
135
|
+
Use **IMAP with XOAUTH2** via the `javanile/php-imap2` library (mirrors the 1.0 monitor),
|
|
136
|
+
**not** Microsoft Graph. The Graph `messages/$count` approach returned HTTP 403 because the
|
|
137
|
+
app lacks Graph `Mail.Read` application consent and Entra consent cannot be obtained. IMAP
|
|
138
|
+
XOAUTH2 uses the app's existing `IMAP.AccessAsApp` permission (the 1.0 monitor already
|
|
139
|
+
relies on it) — no new Entra consent.
|
|
140
|
+
|
|
141
|
+
- OAuth token scope: `https://outlook.office365.com/.default`.
|
|
142
|
+
- Then `imap2_open('{outlook.office365.com:993/imap/ssl/novalidate-cert}INBOX', $mailbox,
|
|
143
|
+
$accessToken, OP_XOAUTH2|OP_READONLY)` → `imap2_num_msg`.
|
|
144
|
+
- Guarded by `function_exists('imap2_open')` so a missing library reports `status:error`
|
|
145
|
+
rather than throwing an uncatchable `Error`.
|
|
146
|
+
- Requires the `[mailbox_compass_statuses]` config group (`tenant_id`, `client_id`,
|
|
147
|
+
`client_secret`, `mailbox`) in each `Config/*.ini`, and **ext-imap enabled** on the
|
|
148
|
+
worker EB tier. The `client_secret` is a credential — it lives in the ini config group,
|
|
149
|
+
never in a doc.
|
|
150
|
+
|
|
151
|
+
## Composer / deploy dependency
|
|
152
|
+
|
|
153
|
+
`worker2/composer.json` added `javanile/php-imap2` (`^0.1.10`) for the IMAP path. The
|
|
154
|
+
`composer require` refreshed `composer.lock`, bumping in-range packages (sentry, aws-sdk,
|
|
155
|
+
phpseclib, phpspreadsheet) and transitive majors (psr/log 1→3, symfony/options-resolver
|
|
156
|
+
5.4→7.4, paragonie/constant_time_encoding 2→3). `composer audit` reported 16 advisories.
|
|
157
|
+
**Review the lockfile diff + `composer audit` before a production deploy.**
|
|
102
158
|
|
|
103
159
|
## Client variations
|
|
104
160
|
|
|
105
|
-
None — the pattern is shared infrastructure.
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
None — the pattern is shared infrastructure. This suite targets Compass USA's Office Depot
|
|
162
|
+
integration, but the class and templates are client-agnostic; clone to provision the same
|
|
163
|
+
check for another client.
|
|
108
164
|
|
|
109
165
|
## Gotchas / known issues
|
|
110
166
|
|
|
111
167
|
- OneUptime Incoming Request bodies are matched **as strings only** (Contains/NotContains) —
|
|
112
168
|
no numeric comparison, no JSON key targeting. Always emit a decided token, never a raw
|
|
113
169
|
number, for push monitors.
|
|
114
|
-
- The OneUptime push URL is a credential — keep it as
|
|
115
|
-
log it or write its value into knowledge docs.
|
|
170
|
+
- The OneUptime push URL is a credential — keep it as an in-file constant in the method;
|
|
171
|
+
never log it or write its value into knowledge docs.
|
|
172
|
+
- **`App_Client_Compass` (1.0) is unavailable in worker2.** Referencing it throws a
|
|
173
|
+
class-not-found `Error`, which `catch (Exception)` does NOT catch — the whole monitor
|
|
174
|
+
dies uncaught. Use `_Worker_Client_Compass::` constants only.
|
|
175
|
+
- `_Query::fetchRow()` returns an object, not an array — access `$row->field`.
|
|
116
176
|
- Keep the metric POST non-fatal (`throwExceptionsOnFailure` off) so a monitoring outage
|
|
117
177
|
never breaks the worker job it rides in.
|
|
178
|
+
- The `agilant-as2` S3 bucket is in **us-west-2**, not the config-default us-east-1 — S3
|
|
179
|
+
monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
|
|
118
180
|
|
|
119
181
|
## Change history
|
|
182
|
+
- 2026-07-14 — Grew `_Worker_Monitor_Compass` to a 9-monitor suite (S3/DB/mailbox);
|
|
183
|
+
finalized the payload contract (`status`/`alarm`/`checkedAtUtc`) and the not-received
|
|
184
|
+
10-min-Degraded / 15-min-Offline + OK-gated recovery criteria; standardized cron on
|
|
185
|
+
`*/5 * * * *` all-hours (120s); chose IMAP XOAUTH2 over Graph for the emailed-ASN mailbox
|
|
186
|
+
(Graph 403, no Entra consent); moved Compass id constants to `_Worker_Client_Compass` and
|
|
187
|
+
registered its DB via `initialize()`; added `javanile/php-imap2`. (jcardinal)
|
|
120
188
|
- 2026-07-13 — Ported Compass monitoring from the 1.0 worker tier into worker2 reporting to
|
|
121
|
-
OneUptime; built `
|
|
122
|
-
|
|
123
|
-
Contains, not compare numbers), and the 5-min cadence → 10/15-min heartbeat timing. (jcardinal)
|
|
189
|
+
OneUptime; built `OfficeDepotEdiImportQueue()`, the token-based alarm contract, and the
|
|
190
|
+
5-min cadence → 10/15-min heartbeat timing. (jcardinal)
|
|
124
191
|
|
|
125
192
|
## Related docs
|
|
126
193
|
- [Monitoring Framework](./monitoring-framework.md) — the parallel DB-driven, email-alert monitoring pattern
|
|
127
194
|
- [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md) — `_Cloud::getS3Objects()` used to list the EDI bucket
|
|
128
195
|
- [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
|
|
196
|
+
</content>
|
|
197
|
+
</invoke>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,8 +17,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 32 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
|
+
- **worker2** (Worker) — 29 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
package/package.json
CHANGED