@enfyra/mcp-server 0.1.69 → 0.1.71

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.
Files changed (248) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/config-local-adapters.d.ts +18 -0
  3. package/dist/lib/config-local-adapters.js +271 -0
  4. package/dist/lib/config-local-adapters.js.map +1 -0
  5. package/dist/lib/config-local-contracts.d.ts +28 -0
  6. package/dist/lib/config-local-contracts.js +200 -0
  7. package/dist/lib/config-local-contracts.js.map +1 -0
  8. package/dist/lib/config-local-prompts.d.ts +10 -0
  9. package/dist/lib/config-local-prompts.js +185 -0
  10. package/dist/lib/config-local-prompts.js.map +1 -0
  11. package/dist/lib/config-local-types.d.ts +23 -0
  12. package/dist/lib/config-local-types.js +2 -0
  13. package/dist/lib/config-local-types.js.map +1 -0
  14. package/dist/lib/config-local.js +3 -649
  15. package/dist/lib/config-local.js.map +1 -1
  16. package/dist/lib/discovery-tools.d.ts +4 -0
  17. package/dist/lib/discovery-tools.js +546 -0
  18. package/dist/lib/discovery-tools.js.map +1 -0
  19. package/dist/lib/dynamic-repository-builder.d.ts +1 -1
  20. package/dist/lib/dynamic-script-context-contract.d.ts +100 -0
  21. package/dist/lib/dynamic-script-context-contract.js +163 -0
  22. package/dist/lib/dynamic-script-context-contract.js.map +1 -0
  23. package/dist/lib/enfyra-mcp-server.d.ts +3 -0
  24. package/dist/lib/enfyra-mcp-server.js +79 -0
  25. package/dist/lib/enfyra-mcp-server.js.map +1 -0
  26. package/dist/lib/enfyra-tool-logic.d.ts +7 -0
  27. package/dist/lib/enfyra-tool-logic.js +8 -0
  28. package/dist/lib/enfyra-tool-logic.js.map +1 -0
  29. package/dist/lib/enfyra-tool-types.d.ts +27 -0
  30. package/dist/lib/enfyra-tool-types.js +2 -0
  31. package/dist/lib/enfyra-tool-types.js.map +1 -0
  32. package/dist/lib/extension-api-builders.d.ts +17 -0
  33. package/dist/lib/extension-api-builders.js +235 -0
  34. package/dist/lib/extension-api-builders.js.map +1 -0
  35. package/dist/lib/extension-component-builders.d.ts +87 -0
  36. package/dist/lib/extension-component-builders.js +623 -0
  37. package/dist/lib/extension-component-builders.js.map +1 -0
  38. package/dist/lib/extension-contract-review.d.ts +26 -0
  39. package/dist/lib/extension-contract-review.js +187 -0
  40. package/dist/lib/extension-contract-review.js.map +1 -0
  41. package/dist/lib/extension-theme-contract.d.ts +63 -0
  42. package/dist/lib/extension-theme-contract.js +417 -0
  43. package/dist/lib/extension-theme-contract.js.map +1 -0
  44. package/dist/lib/identity-tools.d.ts +4 -0
  45. package/dist/lib/identity-tools.js +46 -0
  46. package/dist/lib/identity-tools.js.map +1 -0
  47. package/dist/lib/log-tools.d.ts +4 -0
  48. package/dist/lib/log-tools.js +65 -0
  49. package/dist/lib/log-tools.js.map +1 -0
  50. package/dist/lib/mcp-example-connect.d.ts +9 -0
  51. package/dist/lib/mcp-example-connect.js +442 -0
  52. package/dist/lib/mcp-example-connect.js.map +1 -0
  53. package/dist/lib/mcp-example-extensions.d.ts +9 -0
  54. package/dist/lib/mcp-example-extensions.js +729 -0
  55. package/dist/lib/mcp-example-extensions.js.map +1 -0
  56. package/dist/lib/mcp-example-files.d.ts +9 -0
  57. package/dist/lib/mcp-example-files.js +51 -0
  58. package/dist/lib/mcp-example-files.js.map +1 -0
  59. package/dist/lib/mcp-example-flows.d.ts +9 -0
  60. package/dist/lib/mcp-example-flows.js +76 -0
  61. package/dist/lib/mcp-example-flows.js.map +1 -0
  62. package/dist/lib/mcp-example-graphql.d.ts +9 -0
  63. package/dist/lib/mcp-example-graphql.js +57 -0
  64. package/dist/lib/mcp-example-graphql.js.map +1 -0
  65. package/dist/lib/mcp-example-handlers-hooks.d.ts +9 -0
  66. package/dist/lib/mcp-example-handlers-hooks.js +328 -0
  67. package/dist/lib/mcp-example-handlers-hooks.js.map +1 -0
  68. package/dist/lib/mcp-example-oauth-setup.d.ts +9 -0
  69. package/dist/lib/mcp-example-oauth-setup.js +41 -0
  70. package/dist/lib/mcp-example-oauth-setup.js.map +1 -0
  71. package/dist/lib/mcp-example-permissions-rls.d.ts +9 -0
  72. package/dist/lib/mcp-example-permissions-rls.js +210 -0
  73. package/dist/lib/mcp-example-permissions-rls.js.map +1 -0
  74. package/dist/lib/mcp-example-queries-deep.d.ts +9 -0
  75. package/dist/lib/mcp-example-queries-deep.js +220 -0
  76. package/dist/lib/mcp-example-queries-deep.js.map +1 -0
  77. package/dist/lib/mcp-example-schema-relations.d.ts +9 -0
  78. package/dist/lib/mcp-example-schema-relations.js +148 -0
  79. package/dist/lib/mcp-example-schema-relations.js.map +1 -0
  80. package/dist/lib/mcp-example-websocket.d.ts +9 -0
  81. package/dist/lib/mcp-example-websocket.js +103 -0
  82. package/dist/lib/mcp-example-websocket.js.map +1 -0
  83. package/dist/lib/mcp-examples.d.ts +5 -5
  84. package/dist/lib/mcp-examples.js +22 -2394
  85. package/dist/lib/mcp-examples.js.map +1 -1
  86. package/dist/lib/method-tools.d.ts +4 -0
  87. package/dist/lib/method-tools.js +217 -0
  88. package/dist/lib/method-tools.js.map +1 -0
  89. package/dist/lib/package-tools.d.ts +4 -0
  90. package/dist/lib/package-tools.js +98 -0
  91. package/dist/lib/package-tools.js.map +1 -0
  92. package/dist/lib/platform-data-operations.d.ts +24 -0
  93. package/dist/lib/platform-data-operations.js +71 -0
  94. package/dist/lib/platform-data-operations.js.map +1 -0
  95. package/dist/lib/platform-endpoint-workflow.d.ts +42 -0
  96. package/dist/lib/platform-endpoint-workflow.js +382 -0
  97. package/dist/lib/platform-endpoint-workflow.js.map +1 -0
  98. package/dist/lib/platform-extension-source.d.ts +487 -0
  99. package/dist/lib/platform-extension-source.js +524 -0
  100. package/dist/lib/platform-extension-source.js.map +1 -0
  101. package/dist/lib/platform-extension-tools.d.ts +1 -0
  102. package/dist/lib/platform-extension-tools.js +416 -0
  103. package/dist/lib/platform-extension-tools.js.map +1 -0
  104. package/dist/lib/platform-extension-ui.d.ts +4 -0
  105. package/dist/lib/platform-extension-ui.js +5 -0
  106. package/dist/lib/platform-extension-ui.js.map +1 -0
  107. package/dist/lib/platform-extension-workflow.d.ts +54 -0
  108. package/dist/lib/platform-extension-workflow.js +194 -0
  109. package/dist/lib/platform-extension-workflow.js.map +1 -0
  110. package/dist/lib/platform-flow-operations.d.ts +554 -0
  111. package/dist/lib/platform-flow-operations.js +317 -0
  112. package/dist/lib/platform-flow-operations.js.map +1 -0
  113. package/dist/lib/platform-flow-tools.d.ts +1 -0
  114. package/dist/lib/platform-flow-tools.js +248 -0
  115. package/dist/lib/platform-flow-tools.js.map +1 -0
  116. package/dist/lib/platform-operation-logic.d.ts +10 -0
  117. package/dist/lib/platform-operation-logic.js +11 -0
  118. package/dist/lib/platform-operation-logic.js.map +1 -0
  119. package/dist/lib/platform-operation-tools.d.ts +1 -324
  120. package/dist/lib/platform-operation-tools.js +13 -4620
  121. package/dist/lib/platform-operation-tools.js.map +1 -1
  122. package/dist/lib/platform-operation-types.d.ts +46 -0
  123. package/dist/lib/platform-operation-types.js +2 -0
  124. package/dist/lib/platform-operation-types.js.map +1 -0
  125. package/dist/lib/platform-policy-tools.d.ts +1 -0
  126. package/dist/lib/platform-policy-tools.js +248 -0
  127. package/dist/lib/platform-policy-tools.js.map +1 -0
  128. package/dist/lib/platform-resource-operations.d.ts +124 -0
  129. package/dist/lib/platform-resource-operations.js +109 -0
  130. package/dist/lib/platform-resource-operations.js.map +1 -0
  131. package/dist/lib/platform-resource-tools.d.ts +1 -0
  132. package/dist/lib/platform-resource-tools.js +70 -0
  133. package/dist/lib/platform-resource-tools.js.map +1 -0
  134. package/dist/lib/platform-route-operations.d.ts +222 -0
  135. package/dist/lib/platform-route-operations.js +290 -0
  136. package/dist/lib/platform-route-operations.js.map +1 -0
  137. package/dist/lib/platform-route-tools.d.ts +1 -0
  138. package/dist/lib/platform-route-tools.js +307 -0
  139. package/dist/lib/platform-route-tools.js.map +1 -0
  140. package/dist/lib/platform-shared-operations.d.ts +7 -0
  141. package/dist/lib/platform-shared-operations.js +4 -0
  142. package/dist/lib/platform-shared-operations.js.map +1 -0
  143. package/dist/lib/platform-websocket-tools.d.ts +1 -0
  144. package/dist/lib/platform-websocket-tools.js +70 -0
  145. package/dist/lib/platform-websocket-tools.js.map +1 -0
  146. package/dist/lib/record-tools.d.ts +4 -0
  147. package/dist/lib/record-tools.js +418 -0
  148. package/dist/lib/record-tools.js.map +1 -0
  149. package/dist/lib/required-knowledge.d.ts +1 -1
  150. package/dist/lib/required-knowledge.js +3 -2
  151. package/dist/lib/required-knowledge.js.map +1 -1
  152. package/dist/lib/route-access-tools.d.ts +4 -0
  153. package/dist/lib/route-access-tools.js +172 -0
  154. package/dist/lib/route-access-tools.js.map +1 -0
  155. package/dist/lib/route-definition-tools.d.ts +4 -0
  156. package/dist/lib/route-definition-tools.js +285 -0
  157. package/dist/lib/route-definition-tools.js.map +1 -0
  158. package/dist/lib/route-inspection-tools.d.ts +4 -0
  159. package/dist/lib/route-inspection-tools.js +319 -0
  160. package/dist/lib/route-inspection-tools.js.map +1 -0
  161. package/dist/lib/route-tools.d.ts +1 -0
  162. package/dist/lib/route-tools.js +9 -0
  163. package/dist/lib/route-tools.js.map +1 -0
  164. package/dist/lib/runtime-zone-registry.d.ts +27 -0
  165. package/dist/lib/runtime-zone-registry.js +81 -0
  166. package/dist/lib/runtime-zone-registry.js.map +1 -0
  167. package/dist/lib/runtime-zone-search.d.ts +303 -0
  168. package/dist/lib/runtime-zone-search.js +596 -0
  169. package/dist/lib/runtime-zone-search.js.map +1 -0
  170. package/dist/lib/runtime-zone-tools.d.ts +2 -307
  171. package/dist/lib/runtime-zone-tools.js +2 -674
  172. package/dist/lib/runtime-zone-tools.js.map +1 -1
  173. package/dist/lib/schema-column-tools.d.ts +3 -0
  174. package/dist/lib/schema-column-tools.js +80 -0
  175. package/dist/lib/schema-column-tools.js.map +1 -0
  176. package/dist/lib/schema-mutation-coordinator.d.ts +28 -0
  177. package/dist/lib/schema-mutation-coordinator.js +154 -0
  178. package/dist/lib/schema-mutation-coordinator.js.map +1 -0
  179. package/dist/lib/schema-relation-contracts.d.ts +39 -0
  180. package/dist/lib/schema-relation-contracts.js +160 -0
  181. package/dist/lib/schema-relation-contracts.js.map +1 -0
  182. package/dist/lib/schema-relation-tools.d.ts +3 -0
  183. package/dist/lib/schema-relation-tools.js +61 -0
  184. package/dist/lib/schema-relation-tools.js.map +1 -0
  185. package/dist/lib/schema-table-contracts.d.ts +28 -0
  186. package/dist/lib/schema-table-contracts.js +263 -0
  187. package/dist/lib/schema-table-contracts.js.map +1 -0
  188. package/dist/lib/schema-table-tools.d.ts +3 -0
  189. package/dist/lib/schema-table-tools.js +297 -0
  190. package/dist/lib/schema-table-tools.js.map +1 -0
  191. package/dist/lib/schema-tool-operations.d.ts +150 -0
  192. package/dist/lib/schema-tool-operations.js +468 -0
  193. package/dist/lib/schema-tool-operations.js.map +1 -0
  194. package/dist/lib/schema-tool-types.d.ts +38 -0
  195. package/dist/lib/schema-tool-types.js +2 -0
  196. package/dist/lib/schema-tool-types.js.map +1 -0
  197. package/dist/lib/schema-verification.d.ts +14 -0
  198. package/dist/lib/schema-verification.js +105 -0
  199. package/dist/lib/schema-verification.js.map +1 -0
  200. package/dist/lib/script-tools.d.ts +4 -0
  201. package/dist/lib/script-tools.js +127 -0
  202. package/dist/lib/script-tools.js.map +1 -0
  203. package/dist/lib/system-tools.d.ts +4 -0
  204. package/dist/lib/system-tools.js +41 -0
  205. package/dist/lib/system-tools.js.map +1 -0
  206. package/dist/lib/table-tool-logic.d.ts +5 -0
  207. package/dist/lib/table-tool-logic.js +6 -0
  208. package/dist/lib/table-tool-logic.js.map +1 -0
  209. package/dist/lib/table-tools.d.ts +1 -84
  210. package/dist/lib/table-tools.js +7 -1522
  211. package/dist/lib/table-tools.js.map +1 -1
  212. package/dist/lib/tool-catalog.d.ts +2 -1
  213. package/dist/lib/tool-catalog.js +23 -2
  214. package/dist/lib/tool-catalog.js.map +1 -1
  215. package/dist/lib/tool-metadata-operations.d.ts +27 -0
  216. package/dist/lib/tool-metadata-operations.js +93 -0
  217. package/dist/lib/tool-metadata-operations.js.map +1 -0
  218. package/dist/lib/tool-permission-profile.d.ts +37 -0
  219. package/dist/lib/tool-permission-profile.js +106 -0
  220. package/dist/lib/tool-permission-profile.js.map +1 -0
  221. package/dist/lib/tool-record-operations.d.ts +102 -0
  222. package/dist/lib/tool-record-operations.js +260 -0
  223. package/dist/lib/tool-record-operations.js.map +1 -0
  224. package/dist/lib/tool-route-inspection.d.ts +47 -0
  225. package/dist/lib/tool-route-inspection.js +168 -0
  226. package/dist/lib/tool-route-inspection.js.map +1 -0
  227. package/dist/lib/tool-routing.d.ts +3 -417
  228. package/dist/lib/tool-routing.js +3 -1038
  229. package/dist/lib/tool-routing.js.map +1 -1
  230. package/dist/lib/tool-runtime-config.d.ts +33 -0
  231. package/dist/lib/tool-runtime-config.js +237 -0
  232. package/dist/lib/tool-runtime-config.js.map +1 -0
  233. package/dist/lib/tool-script-operations.d.ts +34 -0
  234. package/dist/lib/tool-script-operations.js +153 -0
  235. package/dist/lib/tool-script-operations.js.map +1 -0
  236. package/dist/lib/workflow-definitions.d.ts +366 -0
  237. package/dist/lib/workflow-definitions.js +703 -0
  238. package/dist/lib/workflow-definitions.js.map +1 -0
  239. package/dist/lib/workflow-planner.d.ts +45 -0
  240. package/dist/lib/workflow-planner.js +338 -0
  241. package/dist/lib/workflow-planner.js.map +1 -0
  242. package/dist/lib/workflow-types.d.ts +39 -0
  243. package/dist/lib/workflow-types.js +2 -0
  244. package/dist/lib/workflow-types.js.map +1 -0
  245. package/dist/mcp-server-entry.d.ts +0 -3
  246. package/dist/mcp-server-entry.js +2 -3249
  247. package/dist/mcp-server-entry.js.map +1 -1
  248. package/package.json +1 -1
@@ -0,0 +1,729 @@
1
+ export const extensionsExamples = {
2
+ title: 'Dynamic app extensions and menus',
3
+ useWhen: 'Use when adding custom Enfyra admin UI pages, widgets, global shell integrations, menu entries, account-panel rows, or shell attention signals.',
4
+ examples: [
5
+ {
6
+ name: 'Create or update HTTP method colors',
7
+ code: `list_methods()
8
+
9
+ create_method({
10
+ method: "PUT",
11
+ buttonColor: "#e0e7ff",
12
+ textColor: "#4338ca"
13
+ })
14
+
15
+ update_method({
16
+ method: "PATCH",
17
+ buttonColor: "#fef3c7",
18
+ textColor: "#b45309"
19
+ })`,
20
+ notes: [
21
+ 'Use dedicated method tools instead of generic CRUD on enfyra_method.',
22
+ 'The backend stores the method label in enfyra_method.name; do not send or filter a `method` field on `enfyra_method`.',
23
+ 'buttonColor is the badge background and textColor is the badge text color.',
24
+ 'The Enfyra admin UI is /settings/methods.',
25
+ 'delete_method is preview-first and should only be used for unused custom methods.',
26
+ ],
27
+ },
28
+ {
29
+ name: 'Create menu then extension',
30
+ code: `ensure_menu({
31
+ label: "Reports",
32
+ type: "Menu",
33
+ path: "/reports",
34
+ icon: "lucide:bar-chart-3",
35
+ order: 20,
36
+ isEnabled: true,
37
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
38
+ permission: JSON.stringify({
39
+ or: [
40
+ { route: "/reports", methods: ["GET"] },
41
+ { route: "/report", methods: ["GET"] }
42
+ ]
43
+ })
44
+ })
45
+
46
+ // Read the created menu id from the tool response, then:
47
+ ensure_page_extension({
48
+ name: "ReportsPage",
49
+ description: "Reports dashboard",
50
+ menuId: "<created-menu-id>",
51
+ code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\\"><article class=\\"eapp-surface-card p-4\\"><div class=\\"flex items-start justify-between gap-3\\"><div><p class=\\"text-sm font-medium eapp-text-tertiary\\">Total</p><p class=\\"mt-2 text-2xl font-semibold eapp-text-primary\\">0</p></div><span class=\\"eapp-primary-soft eapp-icon-tile\\"><span class=\\"eapp-primary-text\\">◆</span></span></div><div class=\\"mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted\\"><div class=\\"eapp-primary-solid h-full w-1/2\\"></div></div></article><article class=\\"eapp-primary-surface eapp-radius-panel border p-4\\"><p class=\\"text-sm font-semibold eapp-text-primary\\">Selected report</p><p class=\\"mt-1 text-sm eapp-text-tertiary\\">Only selected/current identity blocks use identity surface.</p></article></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); const { register: registerHeaderActions } = useHeaderActionRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); registerHeaderActions([{ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', color: 'neutral', variant: 'outline', onClick: () => {}, order: 80 }])</script>",
52
+ isEnabled: true,
53
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
54
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
55
+ })`,
56
+ notes: [
57
+ 'Reports is an illustrative page. Keep the shell/page contracts, but choose the real route, menu label, icon, permissions, and body layout from the operator workflow.',
58
+ 'Menu provides navigation; extension provides content.',
59
+ 'Use enfyra_menu.label, not title.',
60
+ 'Sensitive admin menus should include a permission condition at creation time.',
61
+ 'For page extensions, create the menu first with ensure_menu and pass its id to ensure_page_extension.',
62
+ 'When editing an existing extension by id or name, use update_extension_code so local guards plus /enfyra_extension/preview and the save happen in one atomic call. Do not spend a second LLM step on validate_extension_code followed by update_records unless the user requested validation-only output.',
63
+ 'Call get_extension_theme_contract before writing or reviewing page/widget/global extension UI; that tool is the authority for theme, color, layout, modal, drawer, and shell registry details.',
64
+ 'Call get_enfyra_required_knowledge before saving extension code, pass globalRulesAckKey as globalRulesAckKey, and pass extensionAckKey as extensionKnowledgeAckKey.',
65
+ 'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
66
+ 'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry, destructure register first, then call it with one action or an array.',
67
+ 'Page extensions should be full-bleed and responsive from the first version; the extension root is already inside the Enfyra admin page main.',
68
+ 'Render ordinary metrics and lists in the body, not PageHeader.stats, unless the user explicitly wants a compact overview header.',
69
+ 'Use app theme tokens and Nuxt UI semantic colors by intent; do not hard-code concrete palettes or redefine the app palette inside extension code.',
70
+ 'Use app-owned primitives such as UTabs, CommonModal, CommonDrawer, Widget, useMenuNotificationRegistry, and useAccountPanelRegistry when the workflow matches them.',
71
+ 'Keep list selection local and fetch detail rows only; do not refetch the whole list after a row click unless the list data changed.',
72
+ 'Page extension paths are admin app UI routes. Do not verify them with test_rest_endpoint against ENFYRA_API_URL unless inspect_route shows an API route with the same path.',
73
+ 'After saving, open Enfyra admin tabs should update through the server/Enfyra admin UI realtime reload contract; do not tell the user to refresh unless that contract is proven broken.',
74
+ ],
75
+ },
76
+ {
77
+ name: 'Compose page extensions from widgets',
78
+ code: `// Create reusable/bulky sections as widget extension records first.
79
+ const reportStatusWidgetCode = \`
80
+ <template>
81
+ <section class="eapp-surface-card p-4">
82
+ <div class="flex items-start justify-between gap-3">
83
+ <div>
84
+ <p class="text-sm font-medium eapp-text-tertiary">Total reports</p>
85
+ <p class="mt-2 text-2xl font-semibold eapp-text-primary">{{ total }}</p>
86
+ <p class="mt-1 text-xs eapp-text-tertiary">{{ latestLabel }}</p>
87
+ </div>
88
+ <UButton type="button" color="neutral" variant="outline" @click.stop.prevent="emit('refresh')">Refresh</UButton>
89
+ </div>
90
+ <div class="mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted">
91
+ <div class="eapp-primary-solid h-full" :style="{ width: progressWidth }"></div>
92
+ </div>
93
+ <UButton v-if="hasLatest" type="button" class="mt-3" color="primary" variant="solid" @click.stop.prevent="openLatest">Open latest</UButton>
94
+ </section>
95
+ </template>
96
+
97
+ <script setup>
98
+ const props = defineProps({
99
+ total: { type: Number, default: 0 },
100
+ rows: { type: Array, default: () => [] },
101
+ openDetails: { type: Function, default: null }
102
+ })
103
+ const emit = defineEmits(['refresh'])
104
+ const hasLatest = computed(() => props.rows.length > 0)
105
+ const latestLabel = computed(() => hasLatest.value ? 'Latest: ' + (props.rows[0]?.title || props.rows[0]?.id || 'Untitled') : 'No reports yet')
106
+ const progressWidth = computed(() => hasLatest.value ? '100%' : '0%')
107
+ function openLatest() {
108
+ if (typeof props.openDetails === 'function' && props.rows[0]) props.openDetails(props.rows[0])
109
+ }
110
+ </script>
111
+ \`
112
+
113
+ ensure_widget_extension({
114
+ name: "ReportStatusWidget",
115
+ description: "Report status summary cards",
116
+ code: reportStatusWidgetCode,
117
+ isEnabled: true,
118
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
119
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
120
+ })
121
+
122
+ // Read the created widget record id, then embed it from the page extension.
123
+ ensure_page_extension({
124
+ name: "ReportsPage",
125
+ menuId: "<reports-menu-id>",
126
+ code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'none', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
127
+ isEnabled: true,
128
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
129
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
130
+ })`,
131
+ notes: [
132
+ 'This shows composition mechanics. Replace reports/status/table with domain sections that are independently reusable or complex enough to deserve widgets.',
133
+ 'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
134
+ 'Embed widgets by their numeric enfyra_extension id, not by extensionId/name.',
135
+ 'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
136
+ 'Use kebab-case in the parent template for camelCase widget props, for example :open-details maps to openDetails.',
137
+ 'Do not mutate widget props. Use computed for derived display state, and use watch only when mirroring a prop into local editable draft state.',
138
+ 'Prefer defineEmits for child-to-parent requests such as refresh. Use callback props only for parent-owned modal/drawer openers or imperative navigation.',
139
+ 'Keep PermissionGate and type="button" plus @click.stop.prevent inside action widgets; server permissions still enforce the real boundary.',
140
+ 'the Enfyra admin UI batch-fetches widget metadata requested in the same tick and caches loaded widgets, so render Widget components directly instead of manually fetching widget code.',
141
+ ],
142
+ },
143
+ {
144
+ name: 'Create a global shell extension for app-wide notifications',
145
+ code: `const notificationBellCode = \`
146
+ <template></template>
147
+
148
+ <script setup>
149
+ const unread = ref(0)
150
+ const expanded = ref(false)
151
+
152
+ const notificationDescription = computed(() => {
153
+ if (unread.value > 0) return unread.value === 1 ? '1 unread' : unread.value + ' unread'
154
+ return 'All caught up'
155
+ })
156
+ const notificationBadge = computed(() => unread.value > 0 ? (unread.value > 99 ? '99+' : unread.value) : null)
157
+ const notificationIcon = computed(() => unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell')
158
+
159
+ const NotificationList = defineComponent({
160
+ name: 'NotificationList',
161
+ setup() {
162
+ return () => h('div', { class: 'p-2 text-sm' }, [
163
+ h('button', {
164
+ type: 'button',
165
+ class: 'flex w-full items-center justify-between rounded px-2 py-2 text-left eapp-surface-hover',
166
+ onClick: () => navigateTo('/notifications'),
167
+ }, [
168
+ h('span', 'Open notification center'),
169
+ h('span', { class: 'text-xs eapp-text-tertiary', 'aria-hidden': 'true' }, '→'),
170
+ ]),
171
+ ])
172
+ },
173
+ })
174
+
175
+ const { register } = useAccountPanelRegistry()
176
+ register({
177
+ id: 'notifications',
178
+ order: 20,
179
+ label: 'Notifications',
180
+ icon: notificationIcon,
181
+ description: notificationDescription,
182
+ count: notificationBadge,
183
+ badgeColor: 'error',
184
+ expanded,
185
+ onToggle: () => {
186
+ expanded.value = !expanded.value
187
+ },
188
+ contentComponent: NotificationList,
189
+ })
190
+
191
+ const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
192
+ watchEffect(() => {
193
+ if (notificationBadge.value) {
194
+ registerMenuNotification({
195
+ id: 'notifications-menu-unread',
196
+ target: { path: '/notifications' },
197
+ value: notificationBadge.value,
198
+ color: 'error',
199
+ title: notificationDescription.value,
200
+ })
201
+ } else {
202
+ unregisterMenuNotification('notifications-menu-unread')
203
+ }
204
+ })
205
+
206
+ const { adminSocket } = useAdminSocket()
207
+ const handleNotification = (payload) => {
208
+ if (payload?.unread != null) unread.value = payload.unread
209
+ }
210
+ adminSocket.on('notification:summary', handleNotification)
211
+ onUnmounted(() => {
212
+ adminSocket.off('notification:summary', handleNotification)
213
+ unregisterMenuNotification('notifications-menu-unread')
214
+ })
215
+ </script>
216
+ \`
217
+
218
+ ensure_global_extension({
219
+ name: "NotificationBellGlobal",
220
+ description: "Registers the app-wide notification bell in the account panel",
221
+ code: notificationBellCode,
222
+ isEnabled: true,
223
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
224
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
225
+ })`,
226
+ notes: [
227
+ 'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
228
+ 'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
229
+ 'The notification center is only one possible shell integration. The transferable shape is invisible global extension -> shell registry -> cleanup on unmount.',
230
+ 'Use useMenuNotificationRegistry for sidebar menu counts/dots when notification state should be visible in the menu as well as the notification center.',
231
+ 'Choose value only when the signal source already owns an exact count. Omit value for a dot when realtime only proves that something new exists.',
232
+ 'Do not fetch the destination domain list just to decorate a menu. A mail page fetches mail; a support page fetches tickets; the shell should use notification or summary signals.',
233
+ 'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
234
+ 'For account-panel UI, register data-driven row fields so Enfyra admin UI owns icon size, row spacing, badge placement, hover state, and expanded chrome.',
235
+ 'Use contentComponent only for expanded inner content; use raw component only as an escape hatch when the row cannot fit the shell contract.',
236
+ 'Destructure registry functions and register stable ids so reloads replace the same shell item predictably.',
237
+ 'Remove socket or DOM listeners in onUnmounted; The Enfyra admin UI unmounts old global components when extension cache reloads or the extension is disabled.',
238
+ ],
239
+ },
240
+ {
241
+ name: 'Signal menu attention without polling destination lists',
242
+ code: `const signalBridgeCode = \`
243
+ <template></template>
244
+
245
+ <script setup>
246
+ const attentionRows = ref([])
247
+ const notificationSignal = ref(false)
248
+ const route = useRoute()
249
+
250
+ const notificationApi = useApi('/cloud_admin_notifications', {
251
+ query: {
252
+ filter: { readAt: { _is_null: true } },
253
+ fields: 'id,kind,targetPath,readAt',
254
+ sort: '-createdAt,-id',
255
+ limit: 10,
256
+ },
257
+ })
258
+
259
+ const hasNewEmail = computed(() =>
260
+ attentionRows.value.some((row) => row.kind === 'email_inbound' && !row.readAt)
261
+ )
262
+ const hasNewSupport = computed(() =>
263
+ attentionRows.value.some((row) => row.kind === 'support' && !row.readAt)
264
+ )
265
+ const accountBadge = computed(() => notificationSignal.value ? 'New' : null)
266
+ const accountDescription = computed(() => notificationSignal.value ? 'New admin attention' : 'All caught up')
267
+
268
+ function syncFromNotificationRows() {
269
+ const rows = Array.isArray(notificationApi.data.value?.data)
270
+ ? notificationApi.data.value.data
271
+ : []
272
+ attentionRows.value = rows
273
+ notificationSignal.value = rows.some((row) => !row.readAt)
274
+ }
275
+
276
+ async function refreshNotificationSignals() {
277
+ await notificationApi.execute()
278
+ syncFromNotificationRows()
279
+ }
280
+
281
+ const { register: registerAccountPanel } = useAccountPanelRegistry()
282
+ registerAccountPanel({
283
+ id: 'admin-attention',
284
+ order: 20,
285
+ label: 'Notifications',
286
+ icon: computed(() => notificationSignal.value ? 'lucide:bell-ring' : 'lucide:bell'),
287
+ description: accountDescription,
288
+ count: accountBadge,
289
+ badgeColor: 'info',
290
+ onClick: () => navigateTo('/data/cloud_admin_notifications'),
291
+ })
292
+
293
+ const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
294
+ watchEffect(() => {
295
+ if (hasNewEmail.value) {
296
+ registerMenuNotification({
297
+ id: 'attention-email',
298
+ target: { path: '/email/messages' },
299
+ color: 'info',
300
+ title: 'New inbound email',
301
+ })
302
+ } else {
303
+ unregisterMenuNotification('attention-email')
304
+ }
305
+
306
+ if (hasNewSupport.value) {
307
+ registerMenuNotification({
308
+ id: 'attention-support',
309
+ target: { path: '/cloud/support' },
310
+ color: 'info',
311
+ title: 'New support activity',
312
+ })
313
+ } else {
314
+ unregisterMenuNotification('attention-support')
315
+ }
316
+ })
317
+
318
+ watch(() => route.path, (path) => {
319
+ if (path.startsWith('/email/messages')) {
320
+ attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'email_inbound')
321
+ }
322
+ if (path.startsWith('/cloud/support')) {
323
+ attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'support')
324
+ }
325
+ notificationSignal.value = attentionRows.value.some((row) => !row.readAt)
326
+ })
327
+
328
+ const { adminSocket } = useAdminSocket()
329
+ function handleAdminNotification(payload) {
330
+ refreshNotificationSignals()
331
+ if (payload?.kind === 'email_inbound') {
332
+ registerMenuNotification({ id: 'attention-email', target: { path: '/email/messages' }, color: 'info', title: 'New inbound email' })
333
+ }
334
+ if (payload?.kind === 'support') {
335
+ registerMenuNotification({ id: 'attention-support', target: { path: '/cloud/support' }, color: 'info', title: 'New support activity' })
336
+ }
337
+ }
338
+
339
+ function getAdminSocket() {
340
+ return adminSocket && adminSocket.value !== undefined ? adminSocket.value : adminSocket
341
+ }
342
+
343
+ function bindAdminSocket(socket) {
344
+ if (socket && typeof socket.on === 'function') {
345
+ socket.on('admin:notification-created', handleAdminNotification)
346
+ }
347
+ }
348
+
349
+ function unbindAdminSocket(socket) {
350
+ if (socket && typeof socket.off === 'function') {
351
+ socket.off('admin:notification-created', handleAdminNotification)
352
+ }
353
+ }
354
+
355
+ if (adminSocket && adminSocket.value !== undefined) {
356
+ watch(adminSocket, (nextSocket, previousSocket) => {
357
+ unbindAdminSocket(previousSocket)
358
+ bindAdminSocket(nextSocket)
359
+ })
360
+ }
361
+
362
+ onMounted(() => {
363
+ refreshNotificationSignals()
364
+ bindAdminSocket(getAdminSocket())
365
+ })
366
+ onUnmounted(() => {
367
+ unbindAdminSocket(getAdminSocket())
368
+ unregisterMenuNotification('attention-email')
369
+ unregisterMenuNotification('attention-support')
370
+ })
371
+ </script>
372
+ \`
373
+
374
+ ensure_global_extension({
375
+ name: "AdminAttentionSignalBridge",
376
+ description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
377
+ code: signalBridgeCode,
378
+ isEnabled: true,
379
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
380
+ extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
381
+ })`,
382
+ notes: [
383
+ 'Use this reasoning pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
384
+ 'This example fetches only the notification source of truth, not the email, support, order, or job tables. Substitute your own notification or summary endpoint when available.',
385
+ 'Omitting value on registerMenuNotification renders a dot. That is the right promise when the shell knows "new work exists" but not an exact count.',
386
+ 'If a backend summary event already includes an exact unread count, use value for a count chip. If the event only says one record changed, use a dot and let the page fetch details.',
387
+ 'Map notification kinds to menu targets by product meaning, not by copying these paths. For example, approval_required could target /reviews, failed_job could target /operations/jobs, and quota_warning could target /billing.',
388
+ 'Generalize the lifecycle: seed from a bounded signal source, react to realtime events, clear local attention when the user reaches the owning page, and avoid duplicating that page\'s data fetch.',
389
+ 'Clear local dot signals when the user enters the destination route or when the notification center marks the underlying notification as read.',
390
+ ],
391
+ },
392
+ {
393
+ name: 'Register a data-driven account-panel item',
394
+ code: `<script setup>
395
+ const unread = ref(3)
396
+ const expanded = ref(false)
397
+
398
+ const label = 'Notifications'
399
+ const icon = computed(() => unread.value > 0 ? 'lucide:bell-ring' : 'lucide:bell')
400
+ const count = computed(() => unread.value > 0 ? String(unread.value) : null)
401
+ const description = computed(() => unread.value > 0 ? 'Needs review' : 'All caught up')
402
+
403
+ const NotificationPanelContent = defineComponent({
404
+ name: 'NotificationPanelContent',
405
+ setup() {
406
+ return () => h('div', { class: 'px-2 py-1 text-xs eapp-text-tertiary' }, 'Recent unread notifications can render here.')
407
+ },
408
+ })
409
+
410
+ const { register } = useAccountPanelRegistry()
411
+ register({
412
+ id: 'notifications',
413
+ order: 20,
414
+ label,
415
+ icon,
416
+ description,
417
+ count,
418
+ badgeColor: 'error',
419
+ expanded,
420
+ onToggle: () => {
421
+ expanded.value = !expanded.value
422
+ },
423
+ contentComponent: NotificationPanelContent,
424
+ })
425
+ </script>`,
426
+ notes: [
427
+ 'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
428
+ 'Notifications is illustrative. Account-panel rows can represent any account-scoped attention or shortcut, such as approvals, billing, deployments, or personal tasks.',
429
+ 'Use count for the primary visible badge value. badge remains supported as a legacy alias, but count is what the account trigger aggregates.',
430
+ 'Do not draw a custom full row with page-scale cards, hero headings, large whitespace, or nested buttons unless the shell contract cannot express the UI.',
431
+ 'Let the Enfyra admin UI handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
432
+ 'Keep contentComponent compact; it is rendered inside account-panel chrome and should not create another large card around itself.',
433
+ 'Register the component from a `type="global"` extension, not from a page extension, when it must appear everywhere.',
434
+ ],
435
+ },
436
+ {
437
+ name: 'Page header and action button variants',
438
+ code: `<script setup>
439
+ const { registerPageHeader } = usePageHeaderRegistry()
440
+ const { register: registerHeaderActions } = useHeaderActionRegistry()
441
+
442
+ registerPageHeader({
443
+ title: 'Report detail',
444
+ description: 'Review status, schedule, and delivery history.',
445
+ leadingIcon: 'lucide:file-text',
446
+ gradient: 'none',
447
+ variant: 'minimal'
448
+ })
449
+
450
+ registerHeaderActions([
451
+ {
452
+ id: 'back-to-reports',
453
+ label: 'Reports',
454
+ icon: 'lucide:arrow-left',
455
+ color: 'neutral',
456
+ variant: 'ghost',
457
+ order: 0,
458
+ onClick: () => navigateTo('/reports')
459
+ },
460
+ {
461
+ id: 'send-test-report',
462
+ label: 'Send test',
463
+ icon: 'lucide:send',
464
+ color: 'neutral',
465
+ variant: 'outline',
466
+ order: 1,
467
+ permission: { or: [{ route: '/reports/send-test', methods: ['POST'] }] },
468
+ onClick: sendTest
469
+ },
470
+ {
471
+ id: 'refresh-report',
472
+ label: 'Refresh',
473
+ icon: 'lucide:refresh-cw',
474
+ color: 'neutral',
475
+ variant: 'outline',
476
+ order: 2,
477
+ onClick: refresh
478
+ }
479
+ ])
480
+ </script>`,
481
+ notes: [
482
+ 'Use PageHeader for the title strip; do not render a duplicate header inside extension body.',
483
+ 'The exact actions are illustrative. Choose action prominence from user intent: navigation, secondary utility, primary mutation, or destructive confirmation.',
484
+ 'Use gradient: "none" for generated operational pages; hardcoded named gradients are decorative and should be explicit user intent.',
485
+ 'Back/navigation actions should be neutral ghost so they read as navigation, not a primary operation.',
486
+ 'Visible secondary operations should be neutral outline; soft is only for low-emphasis chrome actions.',
487
+ 'The main page mutation action should be primary solid; refresh is neutral outline unless refresh is the actual primary workflow.',
488
+ 'Do not choose soft only because it looks acceptable in dark mode; light mode must remain clear too.',
489
+ ],
490
+ },
491
+ {
492
+ name: 'Debug menu or extension changes that do not appear in open Enfyra admin tabs',
493
+ code: `// Server side: enfyra_menu and enfyra_extension are runtime UI definitions.
494
+ // They must participate in partial reload, just like metadata/routes.
495
+ // Expected server contract:
496
+ // - cache orchestrator maps enfyra_menu -> menu reload
497
+ // - cache orchestrator maps enfyra_extension -> extension reload
498
+ // - successful writes emit $system:reload to the admin Socket.IO namespace
499
+
500
+ // Enfyra admin UI side expected listener behavior:
501
+ // if reload target is metadata/menu:
502
+ // await fetch menus
503
+ // rebuild menu registry with reset: true
504
+ // invalidate dynamic extension cache too, because route-to-extension mapping may change
505
+ // if reload target is extension or menu:
506
+ // clear dynamic extension component/meta cache
507
+ // reload enabled type="global" shell extensions
508
+
509
+ // Verification pattern:
510
+ // 1. Save the menu or extension record.
511
+ // 2. Watch the open Enfyra admin UI tab for the $system:reload event.
512
+ // 3. Confirm sidebar/menu registry or extension component cache changed.
513
+ // 4. Only use manual reload endpoints or browser refresh after the natural event path is proven stale.`,
514
+ notes: [
515
+ 'Do not treat menu and extension writes as plain CRUD when debugging live admin UI.',
516
+ 'Check both halves: Enfyra Server emits the reload event, and Enfyra admin UI consumes it.',
517
+ 'Menu reload should also invalidate extension cache because menu records attach page extensions to routes.',
518
+ 'Manual reload is a fallback, not the default fix.',
519
+ ],
520
+ },
521
+ {
522
+ name: 'Plan an admin dashboard as multiple pages',
523
+ code: `// Illustrative menu shape for an operations surface:
524
+ ensure_menu({
525
+ type: "Dropdown Menu",
526
+ label: "Operations",
527
+ path: "/operations",
528
+ icon: "lucide:layout-dashboard",
529
+ order: 2,
530
+ isEnabled: true,
531
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>",
532
+ permission: JSON.stringify({
533
+ or: [
534
+ { route: "/operations/jobs", methods: ["GET"] },
535
+ { route: "/enfyra_flow_execution", methods: ["GET"] }
536
+ ]
537
+ })
538
+ })
539
+
540
+ // Child page extensions should be focused:
541
+ // /dashboard compact summary/routing hub: KPIs, current signal, attention queue, navigation cards
542
+ // /operations/jobs background jobs, current step, meaning, next action
543
+ // /operations/orders order/payment status and drill-downs
544
+ // /operations/reports report configuration and delivery history
545
+ // /operations/settings system readiness and configuration
546
+ // Use UTabs inside large pages instead of placing every section in one dashboard.
547
+ // For admin record management, link to /data/<table>, e.g. /data/report, not public website paths.`,
548
+ notes: [
549
+ 'Design the menu/page split before generating dashboard code.',
550
+ 'Operations/jobs/orders/reports/settings are examples of separating mental models. Replace them with the real domains users navigate between.',
551
+ 'Permission-gate sensitive parent dropdown menus too, using any child page route or backing route that represents read access.',
552
+ 'Keep /dashboard as a summary and distribution page, not a detailed operations table.',
553
+ 'Use focused pages for operational domains.',
554
+ 'Each page extension must use usePageHeaderRegistry for the app-shell title strip and should not render a duplicate top header in the body.',
555
+ 'PageHeader.stats is reserved for deliberate overview headers; operational KPIs belong in body cards/tables.',
556
+ 'Operational history pages should not show raw event rows as the primary UI; group by entity/run and translate step keys into operator-facing labels.',
557
+ 'Operational lists should use pagination plus search/filter controls; do not rely on arbitrary fixed limits such as limit=50.',
558
+ 'UTabs is available in the Enfyra admin UI extension runtime for page-level sections.',
559
+ 'Admin links for editing or inspecting records should point to /data/<table> routes.',
560
+ ],
561
+ },
562
+ {
563
+ name: 'Extension fetches Enfyra data',
564
+ code: `<script setup>
565
+ const { data, pending, execute: fetchOrders } = useApi('/order', {
566
+ query: {
567
+ fields: 'id,status,total,createdAt',
568
+ limit: 10,
569
+ sort: '-createdAt'
570
+ },
571
+ errorContext: 'Fetch orders'
572
+ })
573
+
574
+ const orders = computed(() => data.value?.data ?? [])
575
+
576
+ onMounted(() => fetchOrders())
577
+ </script>
578
+
579
+ <template>
580
+ <section class="space-y-3">
581
+ <UButton type="button" color="neutral" variant="outline" :loading="pending" @click="fetchOrders">
582
+ Refresh
583
+ </UButton>
584
+ <div class="eapp-surface-card eapp-divide-y">
585
+ <div v-for="order in orders" :key="order.id" class="px-4 py-3">
586
+ <p class="text-sm font-medium eapp-text-primary">{{ order.status }}</p>
587
+ <p class="text-xs eapp-text-tertiary">{{ order.createdAt }}</p>
588
+ </div>
589
+ </div>
590
+ </section>
591
+ </template>`,
592
+ notes: [
593
+ 'Use app-provided composables in extensions.',
594
+ 'useApi does not auto-run; call execute() on mounted or through an action.',
595
+ 'useApi returns refs. Read normal Enfyra list rows from data.value?.data, or from response?.data when using the direct execute() return value.',
596
+ 'Pass query/body/filter/deep/aggregate as plain objects or computed objects in app/extension code; do not JSON.stringify them for useApi.',
597
+ 'The /order path is illustrative; inspect routes and fetch the smallest data shape the extension needs.',
598
+ 'Keep extension UI focused; move backend logic into handlers/hooks when needed.',
599
+ ],
600
+ },
601
+ {
602
+ name: 'Builder-reviewed modal and drawer footer actions',
603
+ code: `<template>
604
+ <CommonModal
605
+ v-model:open="open"
606
+ :cancel-action="{ label: 'Cancel', onClick: () => (open = false) }"
607
+ :primary-action="{ label: 'Update version', loading: saving, disabled: !canSubmit, onClick: submit }"
608
+ >
609
+ <template #header>
610
+ <h3 class="text-lg font-semibold">Update version</h3>
611
+ </template>
612
+
613
+ <template #body>
614
+ <UInput v-model="version" class="w-full" />
615
+ <UButton
616
+ type="button"
617
+ icon="i-lucide-refresh-cw"
618
+ label="Check version"
619
+ @click.stop.prevent="checkVersion"
620
+ />
621
+ </template>
622
+ </CommonModal>
623
+ </template>`,
624
+ notes: [
625
+ 'Use build_extension_ui with kind=modal or kind=drawer to generate this shape after extension acknowledgement instead of hand-writing the component contract.',
626
+ 'Use build_extension_ui kind=review before saving snippets with modals, drawers, fields, or native buttons.',
627
+ 'Extension validation rejects common field controls without class="w-full" unless marked data-compact or data-inline.',
628
+ ],
629
+ },
630
+ {
631
+ name: 'Extension can use modern browser APIs',
632
+ code: `<script setup lang="ts">
633
+ const statuses = ['active', 'ready']
634
+ const ok = statuses.includes('active')
635
+ const requiredTerms = new Set(['terms', 'privacy'])
636
+ const loaded = await Promise.all([Promise.resolve(1), Promise.resolve(2)])
637
+ const label = String('pending_payment').replace(/_/g, ' ')
638
+ const date = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())
639
+ console.log(ok, requiredTerms.has('terms'), loaded, label, date)
640
+ </script>`,
641
+ notes: [
642
+ 'Do not rewrite extension code to ES5 when tooling rejects modern APIs.',
643
+ 'If diagnostics complain about these APIs, fix Enfyra admin extension TypeScript lib/runtime contract.',
644
+ ],
645
+ },
646
+ {
647
+ name: 'Install and use an app package in an extension',
648
+ code: `install_package({
649
+ name: "dayjs",
650
+ type: "App",
651
+ globalRulesAckKey: "<globalRulesAckKey from get_enfyra_required_knowledge>"
652
+ })
653
+
654
+ // Then in extension code:
655
+ <script setup>
656
+ const formatted = ref('')
657
+
658
+ onMounted(async () => {
659
+ const pkgs = await getPackages(['dayjs'])
660
+ const dayjs = pkgs.dayjs
661
+ formatted.value = dayjs().format('YYYY-MM-DD')
662
+ })
663
+ </script>
664
+
665
+ <template>
666
+ <span>{{ formatted }}</span>
667
+ </template>`,
668
+ notes: [
669
+ 'Install browser-side extension dependencies as type: "App".',
670
+ 'Do not use static import statements in enfyra_extension.code.',
671
+ 'Load app packages with getPackages([...]) inside the extension runtime.',
672
+ 'Use onMounted or an explicit action for package loading when the UI can render a loading state.',
673
+ ],
674
+ },
675
+ {
676
+ name: 'Dashboard aggregate stats with a time range',
677
+ code: `<script setup>
678
+ const range = ref('7d')
679
+ const now = () => new Date()
680
+ const rangeStart = computed(() => {
681
+ const d = now()
682
+ if (range.value === '24h') d.setHours(d.getHours() - 24)
683
+ else if (range.value === '30d') d.setDate(d.getDate() - 30)
684
+ else d.setDate(d.getDate() - 7)
685
+ return d.toISOString()
686
+ })
687
+
688
+ const flowStats = useApi('/enfyra_flow_execution', {
689
+ query: computed(() => ({
690
+ fields: 'id',
691
+ limit: 1,
692
+ meta: 'filterCount',
693
+ filter: { startedAt: { _gte: rangeStart.value } },
694
+ aggregate: {
695
+ id: { count: true },
696
+ status: { count: { _eq: 'failed' } }
697
+ }
698
+ }))
699
+ })
700
+
701
+ const orderStats = useApi('/order', {
702
+ query: computed(() => ({
703
+ fields: 'id',
704
+ limit: 1,
705
+ meta: 'filterCount',
706
+ filter: { createdAt: { _gte: rangeStart.value } },
707
+ aggregate: {
708
+ id: { count: true },
709
+ status: { count: { _eq: 'applied' } },
710
+ amount_usd: { sum: true }
711
+ }
712
+ }))
713
+ })
714
+
715
+ watch(range, () => Promise.all([flowStats.execute(), orderStats.execute()]))
716
+ onMounted(() => Promise.all([flowStats.execute(), orderStats.execute()]))
717
+ </script>`,
718
+ notes: [
719
+ 'Aggregate keys must be real fields or relations.',
720
+ 'Read results from response.meta.aggregate.',
721
+ 'Use top-level filter for time windows and cross-field conditions.',
722
+ 'The flow/order pair is illustrative. Choose aggregates that answer the page question, such as failed work, pending approvals, unread support, quota pressure, or revenue.',
723
+ 'Only aggregate fields and relations that the dashboard is allowed to expose; aggregate values can reveal hidden data even when rows omit that field.',
724
+ 'sum/avg require numeric fields; amount_usd must be a real float/numeric SQL column, not metadata-only float over a varchar physical column.',
725
+ ],
726
+ },
727
+ ],
728
+ };
729
+ //# sourceMappingURL=mcp-example-extensions.js.map