@gxp-dev/tools 2.0.71 → 2.0.73

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,6 +2,96 @@
2
2
 
3
3
  This is a GxP plugin project for the GxP kiosk platform. Follow these guidelines when working with this codebase.
4
4
 
5
+ ## Development Workflow
6
+
7
+ Every task starts with understanding and ends with a validated, linted build. Do not skip steps.
8
+
9
+ ### 1. Understand what the client is building
10
+
11
+ Before touching code, get specific about the feature:
12
+
13
+ - What is the user-facing outcome? Who uses it (attendee, staff, admin)?
14
+ - Which real-world data does it read or write?
15
+ - What events does it react to in real time?
16
+ - What needs to be configurable by an admin (text, assets, colors, thresholds, feature flags)?
17
+
18
+ Ask clarifying questions instead of guessing. A 30-second question beats a 30-minute rewrite.
19
+
20
+ ### 2. Discover the right data sources via MCP
21
+
22
+ Use the `gxp-api` MCP server to ground the implementation in the real platform — never invent endpoints or event names.
23
+
24
+ - **Find endpoints** — `api_list_tags`, `api_list_operation_ids` (optionally scoped by tag), `search_api_endpoints` (keyword).
25
+ - **Inspect a specific endpoint** — `api_get_operation_parameters`, `get_endpoint_details`.
26
+ - **Find endpoints by payload shape** — `api_find_endpoints_by_schema` (search by request/response field names).
27
+ - **Find WebSocket events** — `api_find_events_for_operation` (given an operationId, returns events via `x-triggered-by`), `api_list_events`, `search_websocket_events`, `get_asyncapi_spec`. Run `api_find_events_for_operation` for every operationId you plan to call so you subscribe instead of polling.
28
+ - **Generate dependency JSON** — `api_generate_dependency` produces the canonical entry for `app-manifest.json` → `dependencies`.
29
+ - **Read the docs** — `docs_list_pages`, `docs_search`, `docs_get_page` (full-text search across docs.gxp.dev).
30
+
31
+ Output of this step: a short list of the operationIds, WebSocket events, and dependencies the plugin will use.
32
+
33
+ ### 3. Plan with the client, including the admin config form
34
+
35
+ Before writing code, produce a plan and confirm it with the client. The plan must include:
36
+
37
+ - **Screens/components** — what's rendered and in which layout (Public/Private/System).
38
+ - **Data flow** — which store getters read which sections, which API calls populate them, which socket events mutate state.
39
+ - **Admin configuration form** — every piece of customizable content belongs in `configuration.json` as a card/field so admins can edit it without a code change. Cover text (strings), images (assets), colors/thresholds (settings), and feature toggles.
40
+ - **Strings and assets inventory** — the exact keys that will appear in `app-manifest.json`.
41
+
42
+ Do not proceed to implementation until the client has reviewed the plan.
43
+
44
+ ### 4. Implement
45
+
46
+ Build against the plan:
47
+
48
+ - Use the GxP store for **all** data access — `store.callApi(operationId, identifier, data)` for platform API calls, plus sockets, strings, assets, settings, state. Never use `axios` or `fetch` directly.
49
+ - Declare every permission identifier used by `callApi` in `app-manifest.json` → `dependencies` + `permissions`. Use `"project"` for project-wide / top-level operations and pass any required IDs in `data`.
50
+ - Wire every piece of user-facing text with `gxp-string` and every image with `gxp-src` so the admin form controls them.
51
+ - Keep components under `src/`. The runtime container (layouts, routing, dev tools) is not yours to edit.
52
+
53
+ ### 5. Sync the manifest and build the admin form
54
+
55
+ As soon as you've added a new `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src` — and before you move on to testing — close the loop on the admin-editable surface:
56
+
57
+ 1. **Extract the configurable surface into `app-manifest.json`.** Run the MCP tool `config_extract_strings` with `writeTo: "app-manifest.json"`. It scans `src/` for every `gxp-string`, `gxp-src`, `store.getString/getSetting/getAsset/getState`, and `store.callApi` identifier and merges the new keys into the manifest (linter-guarded; falls back to `gxdev extract-config` on the CLI if you need it). Do this every time you touch the plugin's public surface so the manifest never drifts from the code.
58
+
59
+ 2. **Build `configuration.json` from what's in the manifest.** For every entry in the manifest, add a matching field in the admin form using the MCP `config_*` tools — they validate each write against the schema. Default mappings:
60
+
61
+ | Manifest entry | `configuration.json` field type |
62
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
63
+ | `strings.default.*` | `text` (or `textarea` for long copy) |
64
+ | `assets.*` (driven by `gxp-src`) | `selectAsset` |
65
+ | `dependencies[]` — each identifier | `asyncSelect` wired to the matching resource list endpoint (so the admin binds to a real record) |
66
+ | `settings.*` that represent colors | `colorPicker` |
67
+ | `settings.*` that represent thresholds / numbers | `number` |
68
+ | `settings.*` that represent feature toggles | `boolean` |
69
+ | Anything else discussed with the client | Pick with `config_list_field_types` / `config_get_field_schema` |
70
+
71
+ Group related fields into `fields_list` cards (`config_add_card` + `config_add_field`). If you're unsure what field types exist, call `config_list_field_types` / `config_list_card_types` first. If a mutation is refused, read the validation error and fix the input — do not set `force: true`.
72
+
73
+ 3. **Lint.** Run `gxdev lint --all` and fix every error before moving on.
74
+
75
+ ### 6. Test with real broadcasts
76
+
77
+ Before declaring done, exercise the plugin against real event shapes:
78
+
79
+ - List available events — `gxdev socket list`.
80
+ - Send one — `gxdev socket send --event <EventName>` (edit or add payloads under `socket-events/`).
81
+ - Hit endpoints against the local mock API via the `test_api_route` MCP tool.
82
+ - Scaffold unit tests with `test_scaffold_component_test` for any non-trivial component.
83
+
84
+ ### 7. Final lint
85
+
86
+ Step 5 already runs `gxdev lint --all`, but run it one more time after Step 6's test pass — test changes (adding mock payloads, tweaking identifiers) can re-introduce schema drift.
87
+
88
+ ```bash
89
+ gxdev lint # validate default targets
90
+ gxdev lint --all # validate everything
91
+ ```
92
+
93
+ Fix every error. Do not mark work complete with a failing lint.
94
+
5
95
  ## Project Architecture
6
96
 
7
97
  This plugin runs inside a container environment provided by the `gxdev` development server. You should ONLY modify files in the `src/` directory. The runtime container (PortalContainer.vue) handles layouts, dev tools, routing, and store initialization.
@@ -17,30 +107,102 @@ src/
17
107
 
18
108
  ## Core Principle: Use the GxP Store for Everything
19
109
 
20
- The `gxpPortalConfigStore` is the central hub. Import it in any component:
110
+ The `gxpPortalConfigStore` is the central hub for every piece of data — API, sockets, strings, assets, settings, state. Import it in any component:
21
111
 
22
112
  ```javascript
23
113
  import { useGxpStore } from "@gx-runtime/stores/gxpPortalConfigStore"
24
114
  const store = useGxpStore()
25
115
  ```
26
116
 
27
- ## API Calls - ALWAYS Use Store Methods
117
+ ## API Calls Use `store.callApi` with an Operation ID + Permission Identifier
118
+
119
+ **Every call to the GxP platform goes through `store.callApi`.** It handles authorization, URL resolution, team/project scoping, and path-parameter substitution. You only provide three things:
28
120
 
29
- NEVER use axios or fetch directly. The store handles authentication, base URLs, and CORS:
121
+ ```javascript
122
+ await store.callApi(operationId, identifier, data)
123
+ ```
124
+
125
+ | Argument | Purpose |
126
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `operationId` | The OpenAPI operationId for the call. Look it up with `api_list_operation_ids` or `search_api_endpoints` (MCP). Never invent one. |
128
+ | `identifier` | A **permission identifier** declared in `app-manifest.json` → `dependencies` / `permissions`. Determines which resource's permissions the call runs under, and supplies the parent object ID that gets substituted into the path. |
129
+ | `data` | Additional params — body fields, query params, or path params not covered by the identifier. Pass `"pluginVars.someKey"` as a value to pull from settings at call time. |
130
+
131
+ NEVER use axios/fetch directly. `apiGet`/`apiPost`/`apiPut`/`apiPatch`/`apiDelete` still exist as low-level escape hatches, but `callApi` is the default — only it participates in the permission model.
132
+
133
+ ### Permission identifiers
134
+
135
+ Each identifier is declared in `app-manifest.json` and later bound by an admin to a specific resource + permission scope (read, create, update, delete). Pick identifiers that describe the **role the resource plays in the plugin**, not its name.
136
+
137
+ Example — a plugin that reads posts from one social stream and creates posts on another:
138
+
139
+ ```json
140
+ // app-manifest.json
141
+ {
142
+ "dependencies": [
143
+ { "identifier": "social_stream_one", "model": "SocialStream" },
144
+ { "identifier": "social_stream_two", "model": "SocialStream" }
145
+ ],
146
+ "permissions": [
147
+ {
148
+ "identifier": "social_stream_one",
149
+ "description": "Source stream — read posts"
150
+ },
151
+ {
152
+ "identifier": "social_stream_two",
153
+ "description": "Destination stream — create posts"
154
+ }
155
+ ]
156
+ }
157
+ ```
30
158
 
31
159
  ```javascript
32
- // Correct - use store methods
33
- const data = await store.apiGet("/api/v1/endpoint", { param: "value" })
160
+ // Read posts from the source stream
161
+ const posts = await store.callApi("posts.index", "social_stream_one")
162
+
163
+ // Create a post on the destination stream
164
+ await store.callApi("posts.store", "social_stream_two", {
165
+ body: "Hello world",
166
+ image_url: imageUrl,
167
+ })
168
+ ```
169
+
170
+ The admin assigns `social_stream_one` read-only access to stream A and `social_stream_two` create access to stream B. The plugin code never hard-codes IDs.
171
+
172
+ ### The `project` identifier — project-wide, top-level operations
173
+
174
+ When you need to create the parent resource itself (e.g. the social stream), or hit any endpoint scoped at the project level rather than to a specific dependency, use the reserved identifier `"project"`. `callApi` will run with project-wide permissions — but you must pass any path params the endpoint needs in `data`.
175
+
176
+ ```javascript
177
+ // Create a new social stream under the project
178
+ const stream = await store.callApi("social_streams.store", "project", {
179
+ name: "New Stream",
180
+ })
181
+
182
+ // Create a post directly against a known socialStreamId (no dependency binding)
183
+ await store.callApi("posts.store", "project", {
184
+ socialStreamId: stream.id,
185
+ body: "First post",
186
+ })
187
+ ```
188
+
189
+ Rule of thumb:
190
+
191
+ - Operating on a resource the admin will bind later → declare a dependency identifier and pass it.
192
+ - Creating the parent resource itself, or acting across the whole project → use `"project"` and pass the relevant IDs in `data`.
193
+
194
+ ### Low-level methods (avoid unless necessary)
195
+
196
+ ```javascript
197
+ await store.apiGet("/api/v1/endpoint", { param: "value" })
34
198
  await store.apiPost("/api/v1/endpoint", { data: "value" })
35
199
  await store.apiPut("/api/v1/endpoint/123", { data: "value" })
36
200
  await store.apiPatch("/api/v1/endpoint/123", { data: "value" })
37
201
  await store.apiDelete("/api/v1/endpoint/123")
38
-
39
- // WRONG - never do this
40
- // const response = await axios.get(...); // NO!
41
- // const response = await fetch(...); // NO!
42
202
  ```
43
203
 
204
+ These bypass the permission model. Prefer `callApi`.
205
+
44
206
  ## Store Data Access
45
207
 
46
208
  ```javascript
@@ -58,26 +220,59 @@ store.updateAsset("key", "url")
58
220
  store.updateState("key", "value")
59
221
  ```
60
222
 
61
- ## WebSocket Events
223
+ ## Real-Time Events
224
+
225
+ Every plugin has two streams of real-time events, both surfaced through the store:
226
+
227
+ 1. **The `primary` channel** — a peer channel scoped to users of this plugin right now. Use it for in-app pub/sub (cursor positions, "someone just did X", remote controls).
228
+ 2. **Platform API events** — defined in AsyncAPI at `${apiDocsBaseUrl}/api-specs/asyncapi.json` under `components.messages`. Each message can declare `x-triggered-by` pointing at an OpenAPI operationId. When the server handles that operation, the event fires — you subscribe instead of polling.
62
229
 
63
- Listen for real-time events through the store:
230
+ ### The `primary` channel
64
231
 
65
232
  ```javascript
66
- // Listen on primary socket
67
- store.listenSocket("primary", "EventName", (data) => {
68
- console.log("Received:", data)
233
+ // Listen for a custom event from other users of this plugin
234
+ store.listen("primary", "cursor_moved", (data) => {
235
+ console.log("Peer moved:", data)
69
236
  })
70
237
 
71
- // Emit events
72
- store.emitSocket("primary", "client-event", { data: "value" })
238
+ // Broadcast to everyone else on the primary channel
239
+ store.broadcast("primary", "cursor_moved", { x: 42, y: 100 })
240
+ ```
241
+
242
+ ### Platform API events
243
+
244
+ Use the AsyncAPI form of `listen` — event name first, permission identifier second, callback third:
73
245
 
74
- // For dependency-based sockets
75
- store.useSocketListener("dependency_id", "updated", callback)
246
+ ```javascript
247
+ store.listen("SocialStreamPostCreated", "social_stream_two", (post) => {
248
+ console.log("New post on the destination stream:", post)
249
+ })
76
250
  ```
77
251
 
252
+ The permission identifier (`"social_stream_two"` above, or `"project"` for project-wide events) is the same identifier you pass to `callApi`. It scopes the subscription to the resource the admin bound for that identifier.
253
+
254
+ ### Rule: whenever you add a `callApi`, check for a matching event
255
+
256
+ Before wiring polling or manual refresh after a mutation, ask: does this operation fire a socket event? Use the MCP:
257
+
258
+ - `api_find_events_for_operation` — given an operationId, returns every AsyncAPI message whose `x-triggered-by` matches. If it returns one, subscribe to it instead of polling.
259
+ - `api_list_events` — list every event, optionally filtered by `triggeredBy`.
260
+ - `search_websocket_events` — keyword search across events + channels.
261
+
262
+ Example: you just added `store.callApi("posts.store", "social_stream_two", {...})`. Run `api_find_events_for_operation({ operationId: "posts.store" })`, see `SocialStreamPostCreated`, subscribe with `store.listen("SocialStreamPostCreated", "social_stream_two", cb)`. No polling.
263
+
264
+ ### Testing broadcasts locally
265
+
266
+ ```bash
267
+ gxdev socket list # list available events
268
+ gxdev socket send --event EventName # fire a test broadcast
269
+ ```
270
+
271
+ Payloads live under `socket-events/` — add or edit a JSON file to define a new one.
272
+
78
273
  ## Vue Directives for Dynamic Content
79
274
 
80
- Use these directives instead of hardcoding text and images:
275
+ Every piece of admin-editable content goes through a directive — that's what makes the configuration form meaningful.
81
276
 
82
277
  ```html
83
278
  <!-- Text from strings -->
@@ -90,6 +285,40 @@ Use these directives instead of hardcoding text and images:
90
285
  <img gxp-src="hero_image" src="/placeholder.jpg" alt="Hero" />
91
286
  ```
92
287
 
288
+ ## Admin Configuration Form (`configuration.json`)
289
+
290
+ `configuration.json` defines the form admins use to customize the plugin after it ships. Every string, asset, dependency, color, and setting your plugin exposes must have a matching field here.
291
+
292
+ The full workflow — run it whenever you've added/changed a `callApi`, `listen`, `gxp-string`, or `gxp-src`:
293
+
294
+ 1. **Extract the configurable surface into `app-manifest.json`** — `config_extract_strings` with `writeTo: "app-manifest.json"` (same logic as `gxdev extract-config` on the CLI). It scans `src/` for directives and store usage and merges the new keys into the manifest, linter-guarded.
295
+ 2. **Add a form field in `configuration.json` for every manifest entry**, using the mapping below.
296
+ 3. **Validate** — `config_validate` on demand, or `gxdev lint --all` at the end.
297
+
298
+ ### Default field mapping
299
+
300
+ | Manifest source | `configuration.json` field |
301
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
302
+ | `strings.default.<key>` | `text` (short) or `textarea` (long) |
303
+ | `assets.<key>` (driven by `gxp-src`) | `selectAsset` |
304
+ | `dependencies[]` — each declared identifier | `asyncSelect` bound to the matching resource list endpoint so the admin picks a real record |
305
+ | `settings.<key>` representing a color | `colorPicker` |
306
+ | `settings.<key>` representing a threshold/number | `number` |
307
+ | `settings.<key>` representing a feature toggle | `boolean` |
308
+ | Any other setting discussed with the client | Pick via `config_list_field_types` / `config_get_field_schema` |
309
+
310
+ Group related fields into `fields_list` cards (`config_add_card` then `config_add_field`). Use the `name` of each field to exactly match the manifest key it controls — that's the contract the directives and store getters rely on.
311
+
312
+ ### Relevant tools
313
+
314
+ - `config_list_card_types`, `config_list_field_types` — enumerate what's available.
315
+ - `config_get_field_schema` — get the shape (required props, conditional rules) of a specific field type.
316
+ - `config_add_card`, `config_add_field`, `config_move_field`, `config_remove_field`, `config_move_card`, `config_remove_card` — mutate the form. Linter-guarded; invalid writes are refused.
317
+ - `config_extract_strings` — the manifest-sync entry point described above.
318
+ - `config_validate` — validate a file on demand.
319
+
320
+ If a mutation is refused, read the validation error and fix the input — do not reach for `force: true`.
321
+
93
322
  ## Component Kit
94
323
 
95
324
  Import UI components from `@gramercytech/gx-componentkit`:
@@ -106,13 +335,26 @@ import {
106
335
 
107
336
  Available: GxButton, GxCard, GxInput, GxModal, GxSpinner, GxAlert, GxBadge, GxAvatar, GxProgress, GxTabs, GxAccordion
108
337
 
109
- ## Configuration
338
+ ## Configuration Files
110
339
 
111
- Edit `app-manifest.json` for strings, assets, and settings. Changes hot-reload during development.
340
+ - `app-manifest.json` strings, assets, settings, dependencies, permissions. Hot-reloaded in dev.
341
+ - `configuration.json` — admin-facing form definition (cards + fields).
342
+
343
+ ## Linting
344
+
345
+ Run before declaring any change complete:
346
+
347
+ ```bash
348
+ gxdev lint # default targets
349
+ gxdev lint --all # everything
350
+ gxdev lint --json # machine-readable output
351
+ ```
112
352
 
113
353
  ## Testing
114
354
 
115
355
  - Socket events: `gxdev socket send --event EventName`
356
+ - API calls against local mock: `test_api_route` MCP tool
357
+ - Component tests: `test_scaffold_component_test` MCP tool
116
358
  - Dev Tools: Press Ctrl+Shift+D
117
359
  - Console: `window.gxDevTools.store()` to inspect store
118
360
 
@@ -127,6 +369,8 @@ Set `VITE_API_ENV` in `.env`:
127
369
 
128
370
  ## API Documentation
129
371
 
372
+ Prefer the MCP tools (`get_openapi_spec`, `search_api_endpoints`, `docs_search`, etc.) over fetching these URLs directly:
373
+
130
374
  - OpenAPI Spec: https://api.zenith-develop.env.eventfinity.app/api-specs/openapi.json
131
375
  - AsyncAPI Spec: https://api.zenith-develop.env.eventfinity.app/api-specs/asyncapi.json
132
376
  - Webhooks: https://api.zenith-develop.env.eventfinity.app/api-specs/webhooks.json
@@ -2,6 +2,73 @@
2
2
 
3
3
  This is a GxP plugin project for the GxP kiosk platform built with Vue 3.
4
4
 
5
+ ## Development Workflow
6
+
7
+ Work through these steps in order. Do not skip.
8
+
9
+ ### 1. Understand the feature
10
+
11
+ Before writing anything, clarify with the client:
12
+
13
+ - Who uses it and what's the user-facing outcome?
14
+ - Which real data is read/written? Which real-time events matter?
15
+ - What must an admin be able to customize after ship (text, images, colors, thresholds)?
16
+
17
+ ### 2. Discover data sources via the `gxp-api` MCP server
18
+
19
+ Ground the implementation in real platform endpoints and events. Do not invent API paths or event names.
20
+
21
+ - Endpoints — `api_list_tags`, `api_list_operation_ids`, `search_api_endpoints`, `api_get_operation_parameters`, `get_endpoint_details`.
22
+ - Find by payload shape — `api_find_endpoints_by_schema`.
23
+ - WebSocket events — `api_find_events_for_operation` (maps operationId → events via `x-triggered-by`), `api_list_events`, `search_websocket_events`, `get_asyncapi_spec`. Run for every planned operationId so you subscribe instead of polling.
24
+ - Canonical dependency JSON — `api_generate_dependency`.
25
+ - Documentation — `docs_list_pages`, `docs_search`, `docs_get_page`.
26
+
27
+ ### 3. Plan with the client
28
+
29
+ Confirm a plan before implementing. It must include:
30
+
31
+ - Screens/components and which layout (Public/Private/System).
32
+ - Which store getters, API calls, and socket events power each piece of data.
33
+ - The **admin configuration form** (`configuration.json`) — every admin-editable string, asset, color, and setting listed as cards/fields.
34
+ - The exact keys you'll add to `app-manifest.json`.
35
+
36
+ ### 4. Implement
37
+
38
+ - All data goes through the GxP store — `store.callApi(operationId, identifier, data)` for platform APIs, plus sockets, strings, assets, settings, state. No direct `axios`/`fetch`.
39
+ - Every permission identifier used by `callApi` must be declared in `app-manifest.json` → `dependencies` + `permissions`. Use `"project"` for project-wide / top-level operations.
40
+ - Wire every user-facing text with `gxp-string` and every image with `gxp-src`.
41
+ - Only edit files under `src/`; the runtime container is off-limits.
42
+
43
+ ### 5. Sync the manifest and build the admin form
44
+
45
+ Whenever you add or change a `store.callApi`, `store.listen`, `gxp-string`, or `gxp-src`, close the loop on the admin-editable surface before you move on:
46
+
47
+ 1. **Extract into `app-manifest.json`** — call the MCP tool `config_extract_strings` with `writeTo: "app-manifest.json"` (same logic as `gxdev extract-config` on the CLI). It scans `src/` for directives and store usage and merges new keys into the manifest, linter-guarded.
48
+ 2. **Add a form field in `configuration.json` for every manifest entry**, using the MCP `config_*` tools. Default mapping:
49
+ - `strings.default.*` → `text` (or `textarea` for long copy)
50
+ - `assets.*` (driven by `gxp-src`) → `selectAsset`
51
+ - Each declared dependency identifier → `asyncSelect` bound to the matching resource list endpoint so the admin picks a real record
52
+ - Color settings → `colorPicker`; numeric thresholds → `number`; feature toggles → `boolean`
53
+ - Anything else → enumerate with `config_list_field_types` / `config_get_field_schema`
54
+ Each field's `name` must exactly match the manifest key it controls.
55
+ 3. **Lint** — `gxdev lint --all`. Fix every error before moving on.
56
+
57
+ ### 6. Test broadcasts
58
+
59
+ - `gxdev socket list` — see available events.
60
+ - `gxdev socket send --event <EventName>` — fire a test broadcast.
61
+ - `test_api_route` MCP tool — hit endpoints against the local mock.
62
+ - `test_scaffold_component_test` MCP tool — generate Vitest files.
63
+
64
+ ### 7. Final lint
65
+
66
+ ```bash
67
+ gxdev lint --all
68
+ ```
69
+
70
+ Run again after testing and fix every error before declaring done.
71
+
5
72
  ## Architecture
6
73
 
7
74
  The plugin runs inside a container provided by the `gxdev` server. Only edit files in `src/`:
@@ -9,24 +76,71 @@ The plugin runs inside a container provided by the `gxdev` server. Only edit fil
9
76
  - `src/Plugin.vue` - Main entry point
10
77
  - `src/components/` - Reusable components
11
78
  - `src/views/` - Page components
12
- - `app-manifest.json` - Configuration (strings, assets, settings)
79
+ - `app-manifest.json` - Strings, assets, settings, dependencies (hot-reloaded)
80
+ - `configuration.json` - Admin-facing configuration form (cards + fields)
13
81
 
14
- ## Critical Rule: Use GxP Store for API Calls
82
+ ## Critical Rule: Use `store.callApi` for All Platform Calls
15
83
 
16
- NEVER use axios or fetch directly. Always use the store's API methods:
84
+ Every call to the GxP platform goes through `store.callApi(operationId, identifier, data)`. It handles auth, URL resolution, team/project scoping, and path-parameter substitution.
17
85
 
18
86
  ```javascript
19
87
  import { useGxpStore } from "@gx-runtime/stores/gxpPortalConfigStore"
20
88
  const store = useGxpStore()
21
89
 
22
- // API methods (handles auth, CORS, base URL automatically)
23
- await store.apiGet("/api/v1/endpoint", { params })
24
- await store.apiPost("/api/v1/endpoint", data)
25
- await store.apiPut("/api/v1/endpoint/id", data)
26
- await store.apiPatch("/api/v1/endpoint/id", data)
27
- await store.apiDelete("/api/v1/endpoint/id")
90
+ await store.callApi(operationId, identifier, data)
91
+ ```
92
+
93
+ - **`operationId`** — OpenAPI operationId. Look it up with `api_list_operation_ids` or `search_api_endpoints` (MCP). Do not invent one.
94
+ - **`identifier`** — a permission identifier declared in `app-manifest.json` → `dependencies` / `permissions`. It scopes the call to the resource the admin has bound to that identifier, and supplies the parent ID for path substitution.
95
+ - **`data`** — body fields, query params, or path params not supplied by the identifier. `"pluginVars.someKey"` pulls a value from settings at call time.
96
+
97
+ ### Permission identifier pattern
98
+
99
+ Declare identifiers by the **role** a resource plays, not its name. Example — a plugin that reads posts from one social stream and creates posts on another:
100
+
101
+ ```json
102
+ // app-manifest.json
103
+ {
104
+ "dependencies": [
105
+ { "identifier": "social_stream_one", "model": "SocialStream" },
106
+ { "identifier": "social_stream_two", "model": "SocialStream" }
107
+ ],
108
+ "permissions": [
109
+ { "identifier": "social_stream_one", "description": "Source — read posts" },
110
+ {
111
+ "identifier": "social_stream_two",
112
+ "description": "Destination — create posts"
113
+ }
114
+ ]
115
+ }
28
116
  ```
29
117
 
118
+ ```javascript
119
+ const posts = await store.callApi("posts.index", "social_stream_one")
120
+ await store.callApi("posts.store", "social_stream_two", {
121
+ body: "Hello world",
122
+ })
123
+ ```
124
+
125
+ ### The `"project"` identifier
126
+
127
+ For project-wide operations (creating the parent resource itself, or any top-level call not scoped to a dependency) use the reserved identifier `"project"`. You must pass any required IDs in `data`:
128
+
129
+ ```javascript
130
+ const stream = await store.callApi("social_streams.store", "project", {
131
+ name: "New Stream",
132
+ })
133
+
134
+ await store.callApi("posts.store", "project", {
135
+ socialStreamId: stream.id,
136
+ body: "First post",
137
+ })
138
+ ```
139
+
140
+ ### Low-level methods
141
+
142
+ `store.apiGet` / `apiPost` / `apiPut` / `apiPatch` / `apiDelete` bypass the permission model. Avoid unless you have a specific reason. Never use axios or fetch directly.
143
+
30
144
  ## Store Data Access
31
145
 
32
146
  ```javascript
@@ -43,20 +157,54 @@ store.updateAsset("key", "url")
43
157
  store.updateState("key", "value")
44
158
  ```
45
159
 
46
- ## WebSocket Events
160
+ ## Real-Time Events
161
+
162
+ Two streams, both through the store:
163
+
164
+ 1. **`primary` channel** — peer pub/sub between users of this plugin.
165
+ 2. **Platform API events** — AsyncAPI events from `${apiDocsBaseUrl}/api-specs/asyncapi.json` (`components.messages`). Each message may declare `x-triggered-by` pointing at an OpenAPI operationId.
166
+
167
+ ### `primary` channel
168
+
169
+ ```javascript
170
+ store.listen("primary", "custom_event", (data) => {
171
+ /* ... */
172
+ })
173
+ store.broadcast("primary", "custom_event", { hello: "world" })
174
+ ```
175
+
176
+ ### Platform API events
47
177
 
48
178
  ```javascript
49
- // Listen for events
50
- store.listenSocket("primary", "EventName", (data) => {
51
- console.log("Event received:", data)
179
+ // Event name first, permission identifier second, callback third
180
+ store.listen("SocialStreamPostCreated", "social_stream_two", (post) => {
181
+ // fires whenever the admin-bound social_stream_two receives a new post
52
182
  })
183
+ ```
184
+
185
+ The permission identifier is the same one you use in `callApi` — it scopes the subscription to the admin-bound resource. Use `"project"` for project-wide events.
186
+
187
+ ### Replace polling with events
188
+
189
+ Whenever you add a `callApi` call, check whether its operationId triggers a socket event using MCP tools:
53
190
 
54
- // Emit events
55
- store.emitSocket("primary", "event-name", data)
191
+ - `api_find_events_for_operation { operationId: "posts.store" }` — returns events whose `x-triggered-by` matches.
192
+ - `api_list_events` — list all events; optional `triggeredBy` filter.
193
+ - `search_websocket_events` — keyword search.
194
+
195
+ If a match exists, subscribe instead of polling.
196
+
197
+ ### Testing broadcasts
198
+
199
+ ```bash
200
+ gxdev socket list
201
+ gxdev socket send --event EventName
56
202
  ```
57
203
 
58
204
  ## Vue Directives
59
205
 
206
+ Every admin-editable piece of content goes through a directive — that's the bridge between the component and the admin form.
207
+
60
208
  ```html
61
209
  <!-- Dynamic text from strings -->
62
210
  <h1 gxp-string="welcome_title">Default</h1>
@@ -65,16 +213,30 @@ store.emitSocket("primary", "event-name", data)
65
213
  <img gxp-src="hero_image" src="/placeholder.jpg" />
66
214
  ```
67
215
 
216
+ ## Admin Configuration Form
217
+
218
+ `configuration.json` defines the form admins use to customize the plugin. The workflow — run it whenever you touch a `callApi`, `listen`, `gxp-string`, or `gxp-src`:
219
+
220
+ 1. **Sync the manifest** — `config_extract_strings` with `writeTo: "app-manifest.json"` (same as `gxdev extract-config`). Scans `src/` and merges directives, store getters, and dependency identifiers into the manifest, linter-guarded.
221
+ 2. **Add a form field for every manifest entry** via the MCP `config_*` tools. Default mapping:
222
+ - `strings.default.*` → `text` / `textarea`
223
+ - `assets.*` → `selectAsset`
224
+ - Each `dependencies[]` identifier → `asyncSelect` bound to the resource's list endpoint
225
+ - Colors → `colorPicker`; numbers/thresholds → `number`; toggles → `boolean`
226
+ - Anything else → `config_list_field_types` / `config_get_field_schema`
227
+ Field `name` must match the manifest key it controls.
228
+ 3. **Validate** — `config_validate`, then `gxdev lint --all`.
229
+
230
+ Tools: `config_list_card_types`, `config_list_field_types`, `config_get_field_schema`, `config_add_card`, `config_add_field`, `config_move_field`, `config_remove_field`, `config_validate`, `config_extract_strings`. Every mutation is linter-guarded against schemas in `bin/lib/lint/schemas/`.
231
+
68
232
  ## Component Kit
69
233
 
70
234
  Use `@gramercytech/gx-componentkit` for UI:
71
235
  GxButton, GxCard, GxInput, GxModal, GxSpinner, GxAlert, GxBadge, GxProgress, GxTabs
72
236
 
73
- ## Configuration
74
-
75
- Edit `app-manifest.json` for strings, assets, settings. Hot-reloads in dev.
76
-
77
237
  ## API Specs
78
238
 
239
+ Prefer the MCP tools over direct fetches:
240
+
79
241
  - OpenAPI: https://api.zenith-develop.env.eventfinity.app/api-specs/openapi.json
80
242
  - AsyncAPI: https://api.zenith-develop.env.eventfinity.app/api-specs/asyncapi.json