toga-ai 1.0.196 → 1.0.198
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/ai-bdr/features/call-orchestration.md +18 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/vapi-webhook-handler.md +156 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/standalone/apps/forward/INDEX.md +1 -1
- package/knowledge/standalone/apps/forward/features/design-demo-admin.md +40 -3
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
related:
|
|
18
18
|
- ../architecture.md
|
|
19
19
|
- vapi-integration.md
|
|
20
|
+
- ../../worker2/features/vapi-webhook-handler.md
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
## Summary
|
|
@@ -204,8 +205,23 @@ gracefully handles `"Information not available"`.
|
|
|
204
205
|
|
|
205
206
|
## Where the PHP code lives
|
|
206
207
|
|
|
207
|
-
**
|
|
208
|
-
|
|
208
|
+
**Now committed in `worker2`** as `_Worker_Vapi` (`worker2/Worker/Vapi.php`,
|
|
209
|
+
the webhook) alongside the outbound dialer `_Worker_Ai_Bdr_Vapi`
|
|
210
|
+
(`worker2/Worker/Ai/Bdr/Vapi.php`). Full detail:
|
|
211
|
+
**`2.0/apps/worker2/features/vapi-webhook-handler.md`**.
|
|
212
|
+
|
|
213
|
+
> **Corrections confirmed against the live `_Worker_Vapi` code (2026-06-24):**
|
|
214
|
+
> 1. **Structured output is in `artifact.structuredOutputs`** (keyed by a random
|
|
215
|
+
> UUID; first entry's `result`), **not** `analysis.structuredData` —
|
|
216
|
+
> `analysis{}` is always empty in practice.
|
|
217
|
+
> 2. **The webhook join key is `assistantOverrides.metadata.contactAttemptUuid`**
|
|
218
|
+
> (the dialer pre-creates the `ContactAttempt` and passes its UUID). `call.id`
|
|
219
|
+
> is stored only as `c_vapiCallIdentifier`, not used as the join key.
|
|
220
|
+
> 3. **Filter on `message.type`** — only `end-of-call-report` is processed;
|
|
221
|
+
> `status-update`/`hang`/etc. are acknowledged and skipped. Inbound and test
|
|
222
|
+
> calls carry no `contactAttemptUuid` and must be skipped gracefully.
|
|
223
|
+
|
|
224
|
+
Historical (pre-commit) shape guess:
|
|
209
225
|
|
|
210
226
|
- 1.0: an `App_Controller_*` for the webhook (under the framework's HTTP
|
|
211
227
|
surface), with an `App_Worker_*` or scheduled cron for cadence.
|
|
@@ -15,4 +15,5 @@
|
|
|
15
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 |
|
|
16
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 |
|
|
17
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 |
|
|
18
|
+
| [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
|
|
18
19
|
| [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)
|
|
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: [snaredla]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Vapi.php
|
|
13
|
+
- worker2/Worker/Ai/Bdr/Vapi.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../ai-bdr/features/call-orchestration.md
|
|
16
|
+
- ../../ai-bdr/features/vapi-integration.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
`_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the
|
|
22
|
+
AI-BDR call loop** — the webhook that receives VAPI's end-of-call report, writes the call
|
|
23
|
+
results back onto the originating `ContactAttempt`, and dispatches the post-call action
|
|
24
|
+
(book meeting / schedule callback / nurture / DNC). This is the concrete handler that the
|
|
25
|
+
`ai-bdr` repo's `call-orchestration.md` referred to as "lives in worker/worker2 — add a
|
|
26
|
+
doc when committed."
|
|
27
|
+
|
|
28
|
+
It is the **counterpart** to the outbound dialer `_Worker_Ai_Bdr_Vapi`
|
|
29
|
+
([worker2/Worker/Ai/Bdr/Vapi.php](worker2/Worker/Ai/Bdr/Vapi.php)), which places the calls.
|
|
30
|
+
|
|
31
|
+
## Key files / entry points
|
|
32
|
+
|
|
33
|
+
| File | Role |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `worker2/Worker/Vapi.php` (`_Worker_Vapi`) | Inbound **webhook** — processes `end-of-call-report`, writes back, dispatches actions |
|
|
36
|
+
| `worker2/Worker/Ai/Bdr/Vapi.php` (`_Worker_Ai_Bdr_Vapi`) | Outbound **dialer** — creates the `ContactAttempt`, places the VAPI call |
|
|
37
|
+
|
|
38
|
+
- Action route: `Vapi/Webhook` → `webhook.togahub.com/vapi` (VAPI server messages POST here).
|
|
39
|
+
- `initialize()` registers the `Client_True` DB connection (`_underscore::DB_CLIENT`).
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. **`Webhook($payload, $headers)`** — JSON-decodes the body, takes `message`, hands off to
|
|
44
|
+
`processVapiEndOfCallPayload()`.
|
|
45
|
+
2. **`processVapiEndOfCallPayload()`**:
|
|
46
|
+
- Reads `call.assistantOverrides.metadata.contactAttemptUuid` (falls back to
|
|
47
|
+
`call.metadata`).
|
|
48
|
+
- Loads `_Model_True_ContactAttempt` by that UUID. This is the **join key** back to our DB.
|
|
49
|
+
- Extracts structured output via `extractVapiStructuredOutput()`.
|
|
50
|
+
- `updateContactAttempt()` writes the results, then `executePostCallActions()` dispatches.
|
|
51
|
+
3. **`extractVapiStructuredOutput($artifact)`** — returns the first
|
|
52
|
+
`artifact.structuredOutputs[*].result` array. **`analysis{}` is always empty — the data
|
|
53
|
+
lives in `artifact.structuredOutputs`, keyed by a random UUID.**
|
|
54
|
+
4. **`updateContactAttempt()`** — maps `structured.phpWorkerAction` → `c_callOutcome` /
|
|
55
|
+
`c_actionToTake` via `$actionMap`; builds a payload of only non-empty `c_*` fields (so
|
|
56
|
+
blanks never overwrite existing data); converts VAPI ISO timestamps to **Central Time**;
|
|
57
|
+
persists via `_Component_Api_Toga::send('PUT', '/contact-attempts/{uuid}', …)` (Toga 2.0
|
|
58
|
+
REST API, **not** a direct DB write); also clears the contact's `dtNextContactRequested`.
|
|
59
|
+
5. **`executePostCallActions()`** — `switch ($contactAttempt->c_actionToTake)`:
|
|
60
|
+
- `BOOK_CALCOM_MEETING` → `actionCalComBookMeeting` (Cal.com booking + queue
|
|
61
|
+
`Ai/Bdr/Netsuite/createLeadFromBookedMeeting` + mark COMPLETED)
|
|
62
|
+
- `SCHEDULE_CALLBACK` → `actionScheduleCallback` (sets `dtNextContactRequested` +
|
|
63
|
+
`contactCallTypeId=CALLBACK`)
|
|
64
|
+
- `SEND_NURTURE_EMAIL` → `actionSendNurtureEmail` (queue
|
|
65
|
+
`Ai/Bdr/Netsuite/createLeadFromNurtureEmail` + mark COMPLETED)
|
|
66
|
+
- `DO_NOT_CALL` → `actionFlagDnc` (`isOkayToCall=0` + EXCLUDE from PENDING/ACTIVE campaigns)
|
|
67
|
+
- `NO_ACTION` / default → reset the campaign-contact to PENDING
|
|
68
|
+
|
|
69
|
+
### phpWorkerAction → outcome/action map
|
|
70
|
+
|
|
71
|
+
| `phpWorkerAction` | `c_callOutcome` | `c_actionToTake` |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `BOOK_CALCOM_MEETING` | MEETING_BOOKED | BOOK_CALCOM_MEETING |
|
|
74
|
+
| `SCHEDULE_CALLBACK` | CALLBACK_SCHEDULED | SCHEDULE_CALLBACK |
|
|
75
|
+
| `SEND_NURTURE_EMAIL` | NURTURE_REQUESTED | SEND_NURTURE_EMAIL |
|
|
76
|
+
| `FLAG_DNC_REMOVE_FROM_CAMPAIGNS` | DNC_REQUESTED | DO_NOT_CALL |
|
|
77
|
+
| `MARK_NOT_INTERESTED` | NOT_INTERESTED | DO_NOT_CALL |
|
|
78
|
+
| `MARK_WRONG_PERSON` | WRONG_PERSON | DO_NOT_CALL |
|
|
79
|
+
| `MARK_VOICEMAIL_RETRY` | VOICEMAIL | NO_ACTION |
|
|
80
|
+
| `MARK_NO_ANSWER_RETRY` | NO_ANSWER | NO_ACTION |
|
|
81
|
+
| `MARK_GATEKEEPER_BLOCKED_RETRY` | GATEKEEPER_BLOCKED | NO_ACTION |
|
|
82
|
+
|
|
83
|
+
## The contactAttemptUuid write-back mechanism
|
|
84
|
+
|
|
85
|
+
`contactAttemptUuid` is the **sole linkage** between a VAPI call and our DB:
|
|
86
|
+
|
|
87
|
+
1. The dialer (`_Worker_Ai_Bdr_Vapi`) creates the `ContactAttempt` row **before** placing the
|
|
88
|
+
call ([Ai/Bdr/Vapi.php:438](worker2/Worker/Ai/Bdr/Vapi.php#L438),
|
|
89
|
+
[:694](worker2/Worker/Ai/Bdr/Vapi.php#L694)) and passes its UUID in
|
|
90
|
+
`assistantOverrides.metadata.contactAttemptUuid`
|
|
91
|
+
([Ai/Bdr/Vapi.php:628](worker2/Worker/Ai/Bdr/Vapi.php#L628)).
|
|
92
|
+
2. VAPI echoes that same metadata onto **every** server event for the call.
|
|
93
|
+
3. The webhook loads the row by that UUID and writes results back to it.
|
|
94
|
+
|
|
95
|
+
`contactId` and `campaignId` are read from the **loaded `ContactAttempt` row**, never from the
|
|
96
|
+
webhook payload. `call.id` is stored only as `c_vapiCallIdentifier`.
|
|
97
|
+
|
|
98
|
+
## Message-type filtering (the bug this doc was born from)
|
|
99
|
+
|
|
100
|
+
VAPI sends the webhook **many** server-message types: `status-update`, `hang`,
|
|
101
|
+
`speech-update`, `end-of-call-report`, etc. **Only `end-of-call-report` carries the call data
|
|
102
|
+
(`artifact` / structured output) and should be processed.** The handler must guard on
|
|
103
|
+
`message.type === 'end-of-call-report'` in `Webhook()` and acknowledge (`return 'ok'`) all
|
|
104
|
+
other types before they reach `processVapiEndOfCallPayload()`.
|
|
105
|
+
|
|
106
|
+
- **Regression origin:** commit `583f6be` "Added remaining webhooks" (2026-05-14) created this
|
|
107
|
+
file and broadened the VAPI subscription to all server-message types. Without a type guard,
|
|
108
|
+
every event was processed as an end-of-call report. First Sentry errors appeared ~late May.
|
|
109
|
+
- **Symptom:** `Warning: Undefined property: stdClass::$contactAttemptUuid` (promoted to an
|
|
110
|
+
`ErrorException` by the framework error handler, captured by Sentry).
|
|
111
|
+
|
|
112
|
+
## Inbound / test calls vs worker-placed outbound calls
|
|
113
|
+
|
|
114
|
+
The metadata shape depends on **who placed the call**, not the event type:
|
|
115
|
+
|
|
116
|
+
- **Outbound (our dialer)** → metadata carries `contactAttemptUuid` (every event type,
|
|
117
|
+
including status-updates).
|
|
118
|
+
- **Inbound calls and dashboard test calls** → **no `contactAttemptUuid`** (they never went
|
|
119
|
+
through our dialer, so no `ContactAttempt` was pre-created). Test calls may instead carry
|
|
120
|
+
hand-typed metadata like `{contactId, campaignId, attemptNumber}`.
|
|
121
|
+
|
|
122
|
+
Therefore an `end-of-call-report` can legitimately arrive **without** a `contactAttemptUuid`
|
|
123
|
+
(inbound/test). Those have no attempt to write back to and should be **skipped gracefully**
|
|
124
|
+
(`return 'ok'`), not thrown — otherwise they raise `Contact Attempt not found` and fail the
|
|
125
|
+
job. `Contact Attempt not found` should be reserved for a report that *has* a
|
|
126
|
+
`contactAttemptUuid` but no matching row (a real data-integrity anomaly).
|
|
127
|
+
|
|
128
|
+
## Data model
|
|
129
|
+
|
|
130
|
+
Writes to `ContactAttempts` (via Toga 2.0 API) — `dtStarted`, `dtEnded`, `transcript`,
|
|
131
|
+
`contactCallTypeId`, and many `c_*` fields (`c_callOutcome`, `c_actionToTake`,
|
|
132
|
+
`c_prospectSentiment`, `c_qualificationScore`, `c_callSummary`, `c_meetingTime`,
|
|
133
|
+
`c_meetingType`, `c_contactEmail`, `c_callbackTime`, `c_recordingUrl`, `c_callCost`,
|
|
134
|
+
`c_vapiCallIdentifier`, `c_dncRequested`, etc.). Also updates `Contacts`
|
|
135
|
+
(`dtNextContactRequested`, `contactCallTypeId`, `isOkayToCall`) and `Campaigns_Contacts`
|
|
136
|
+
status. See the `ai-bdr` skill / `_Model_True_ContactAttempt` for the full field list.
|
|
137
|
+
|
|
138
|
+
## Gotchas / known issues
|
|
139
|
+
|
|
140
|
+
- **Structured data is in `artifact.structuredOutputs`, not `analysis.structuredData`.**
|
|
141
|
+
`analysis{}` is always empty. (Corrects the older `ai-bdr/call-orchestration.md`.)
|
|
142
|
+
- **Join key is `contactAttemptUuid`, not `call.id`.** `call.id` is only stored as
|
|
143
|
+
`c_vapiCallIdentifier`.
|
|
144
|
+
- **No message-type guard = errors.** Always filter to `end-of-call-report` first.
|
|
145
|
+
- **Persistence is via the Toga REST API**, not a direct model `save()` — failures are
|
|
146
|
+
caught and logged, not re-thrown.
|
|
147
|
+
- **Timestamps are stored in Central Time**; Cal.com booking converts `c_meetingTime` back to
|
|
148
|
+
UTC.
|
|
149
|
+
|
|
150
|
+
## Related docs
|
|
151
|
+
- [AI-BDR Call Orchestration (the PHP↔VAPI contract)](../../ai-bdr/features/call-orchestration.md)
|
|
152
|
+
- [VAPI Integration — assistants, tools, structured output](../../ai-bdr/features/vapi-integration.md)
|
|
153
|
+
|
|
154
|
+
## Change history
|
|
155
|
+
- 2026-06-24 — Initial doc: webhook handler, contactAttemptUuid write-back, message-type
|
|
156
|
+
filtering bug, inbound/test vs outbound metadata. (snaredla)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -16,7 +16,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
18
|
- **_underscore** (_Underscore) _(framework core)_ — 14 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
|
-
- **worker2** (Worker) —
|
|
19
|
+
- **worker2** (Worker) — 15 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)
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Forwarder Architecture](architecture.md) | Forwarder is a tiny **standalone PHP application** that powers TOGA's short, branded redirect domains. | forward/forward.ini, forward/index.php, forward/.htaccess, forward/.platform/httpd/conf.d/rewritemap.conf, forward/.ebextensions/rewritemap.config, forward/design/index.php, forward/composer.json |
|
|
6
|
-
| [Design Demo Admin Tool](features/design-demo-admin.md) | A standalone, dependency-free PHP tool served at `https://demo.togatech.com/design` that lets the **design team** publish self-contained "Claude Design" HTML ex | forward/design/index.php, forward/design/.config.example.php, forward/.gitignore, forward/.htaccess, forward/.ebextensions/php-uploads.config |
|
|
6
|
+
| [Design Demo Admin Tool](features/design-demo-admin.md) | A standalone, dependency-free PHP tool served at `https://demo.togatech.com/design` that lets the **design team** publish self-contained "Claude Design" HTML ex | forward/design/index.php, forward/tabs.json, forward/design/.config.example.php, forward/.gitignore, forward/.htaccess, forward/.ebextensions/php-uploads.config |
|
|
7
7
|
| [Encrypted-Link Handler](features/encrypted-link-handler.md) | A `index.php` feature in [Forwarder](../architecture.md) for redirect domains whose URL path carries an **encrypted token** that must be decoded before redirect | forward/index.php |
|
|
8
8
|
| [Static Demo Hosting](features/static-demo-hosting.md) | A lightweight way to host self-contained static HTML pages (demos, exported designs, download landing pages) on the Forwarder app under a clean URL — e.g. | forward/.htaccess, forward/togadesk/index.html |
|
|
@@ -6,10 +6,11 @@ project: Forwarder
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- forward/design/index.php
|
|
13
|
+
- forward/tabs.json
|
|
13
14
|
- forward/design/.config.example.php
|
|
14
15
|
- forward/.gitignore
|
|
15
16
|
- forward/.htaccess
|
|
@@ -94,9 +95,34 @@ auto-deploy ("live in a few minutes").
|
|
|
94
95
|
brief "Copied ✓" confirmation.
|
|
95
96
|
- **Copy Claude Design prompt** — one-click copy of the standard self-contained-HTML export
|
|
96
97
|
prompt (the same prompt documented in [static-demo-hosting](static-demo-hosting.md)).
|
|
98
|
+
- **Create a tab** (`apiCreateTab`, action `create-tab`) — appends a named tab to
|
|
99
|
+
`tabs.json`. Case-insensitive dedupe; name length-capped 40. Commits `tabs.json` only.
|
|
100
|
+
- **Move a project to a tab** (`apiSetProjectTab`, action `set-project-tab`) — sets/clears a
|
|
101
|
+
project's `tab` field; empty string = Unsorted (the field is `unset`). Commits that
|
|
102
|
+
project's `project.json` **only** — tab membership does not affect the redirect stub, so
|
|
103
|
+
**no stub rebuild**. `apiCreateProject` also accepts an optional `tab`.
|
|
97
104
|
|
|
98
105
|
The full action set is: `list`, `create-project`, `upload`, `adopt`, `set-latest`,
|
|
99
|
-
`toggle-version`, `edit-version`, `toggle-project` (plus
|
|
106
|
+
`toggle-version`, `edit-version`, `toggle-project`, `create-tab`, `set-project-tab` (plus
|
|
107
|
+
`bootstrap`). `list` also returns the tab list (`{ projects, tabs }`).
|
|
108
|
+
|
|
109
|
+
### Tab-based organization
|
|
110
|
+
|
|
111
|
+
Demo projects can be grouped into named **tabs** (e.g. separate Production / Pipeline /
|
|
112
|
+
Sandbox).
|
|
113
|
+
|
|
114
|
+
- **`tabs.json`** at the repo root holds the ordered tab list — `{"tabs": ["Production",
|
|
115
|
+
"Pipeline", "Sandbox"]}`. `loadTabs()` returns those three as **defaults when `tabs.json`
|
|
116
|
+
does not exist yet** (so tabs appear before any are created); the file is only written on
|
|
117
|
+
the first tab mutation (`create-tab`).
|
|
118
|
+
- Each `project.json` gains an optional **`tab`** field (string). Missing/absent =
|
|
119
|
+
**"Unsorted"**.
|
|
120
|
+
- **UI:** a filter chip bar (All / each tab / Unsorted / + New tab) with per-tab project
|
|
121
|
+
counts; clicking a chip filters the list. Each project card has a **Tab `<select>`** to
|
|
122
|
+
move the project between tabs (on the select's `change`). The New Project modal has a Tab
|
|
123
|
+
dropdown defaulting to the currently-viewed tab.
|
|
124
|
+
- The **displayed** tab list is the **union** of `tabs.json` order plus any tab names
|
|
125
|
+
referenced by projects — so removing a tab from `tabs.json` never orphans a project.
|
|
100
126
|
|
|
101
127
|
### Naming & validation
|
|
102
128
|
|
|
@@ -117,7 +143,10 @@ anyone who reaches it can publish or unlist. Do not treat the tool as protected.
|
|
|
117
143
|
|
|
118
144
|
## Data model
|
|
119
145
|
|
|
120
|
-
`project.json` per managed demo (see Versioning model above)
|
|
146
|
+
`project.json` per managed demo (see Versioning model above), plus an optional `tab` string
|
|
147
|
+
field per project (absent = Unsorted). A single repo-root **`tabs.json`** —
|
|
148
|
+
`{"tabs": [...]}` — holds the ordered tab list (defaults to `["Production", "Pipeline",
|
|
149
|
+
"Sandbox"]` until first written). No database.
|
|
121
150
|
|
|
122
151
|
## Client variations
|
|
123
152
|
|
|
@@ -158,6 +187,14 @@ None — internal/shared design-team tool.
|
|
|
158
187
|
`isValidVersionId` + length caps). Added a **Copy URL** button on each project card
|
|
159
188
|
(`navigator.clipboard` → `https://demo.togatech.com/<project>`). Both additive; no new
|
|
160
189
|
security surface. (jcardinal)
|
|
190
|
+
- 2026-06-25 — Added **tab-based organization**: new repo-root `tabs.json` (ordered tab
|
|
191
|
+
list, defaults Production/Pipeline/Sandbox until first written via `create-tab`), optional
|
|
192
|
+
`tab` field on `project.json` (absent = Unsorted), and actions **`create-tab`** /
|
|
193
|
+
**`set-project-tab`** (`apiCreateProject` accepts an optional `tab`). UI filter chip bar
|
|
194
|
+
with per-tab counts, per-card Tab select, and a Tab dropdown on the New Project modal. The
|
|
195
|
+
displayed tab list is the union of `tabs.json` and tabs referenced by projects, so dropping
|
|
196
|
+
a tab from `tabs.json` never orphans a project. `set-project-tab` commits `project.json`
|
|
197
|
+
only (no stub rebuild). (jcardinal)
|
|
161
198
|
|
|
162
199
|
## Related docs
|
|
163
200
|
|
package/package.json
CHANGED