@intranefr/superbackend 1.4.4 → 1.5.1

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 (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -0,0 +1,741 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>UI Components - Admin</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ </head>
10
+ <body class="bg-gray-100">
11
+ <div id="app" class="min-h-screen">
12
+ <div class="bg-white shadow">
13
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
14
+ <div class="flex justify-between items-center">
15
+ <div>
16
+ <h1 class="text-2xl font-bold text-gray-900">UI Components</h1>
17
+ <p class="text-sm text-gray-600 mt-1">Manage projects, UI components, and assignments.</p>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
24
+ <div v-if="toast.show" class="fixed top-4 right-4 z-50">
25
+ <div :class="['px-4 py-3 rounded shadow text-sm', toast.type === 'error' ? 'bg-red-600 text-white' : 'bg-green-600 text-white']">
26
+ {{ toast.message }}
27
+ </div>
28
+ </div>
29
+
30
+ <div class="bg-white rounded-lg shadow p-4">
31
+ <div class="flex items-center justify-between">
32
+ <div>
33
+ <h2 class="font-semibold text-gray-800">Using the system endpoints & SDK</h2>
34
+ <p class="text-xs text-gray-500">Quick reference for public endpoints and the browser SDK.</p>
35
+ </div>
36
+ <button
37
+ @click="toggleHelp"
38
+ class="text-xs px-2 py-1 rounded border border-gray-200 bg-gray-50 hover:bg-gray-100 text-gray-700"
39
+ >
40
+ {{ helpOpen ? 'Hide' : 'Show' }}
41
+ </button>
42
+ </div>
43
+
44
+ <div v-if="helpOpen" class="mt-3 space-y-4 text-sm text-gray-700">
45
+ <div>
46
+ <h3 class="font-semibold text-gray-800 mb-1">Public API endpoints</h3>
47
+ <p class="text-xs text-gray-500 mb-2">All paths are relative to this backend base URL.</p>
48
+ <pre class="text-[11px] bg-gray-50 border border-gray-200 rounded p-2 whitespace-pre-wrap break-words"><code>
49
+ GET {{ baseUrl }}/api/ui-components/manifest/:projectId
50
+ GET {{ baseUrl }}/api/ui-components/component/:code
51
+
52
+ // Private projects require an API key header
53
+ // x-project-key: YOUR_PROJECT_API_KEY
54
+
55
+ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
56
+ -H "x-project-key: YOUR_PROJECT_API_KEY"
57
+ </code></pre>
58
+ </div>
59
+
60
+ <div>
61
+ <h3 class="font-semibold text-gray-800 mb-1">Browser SDK</h3>
62
+ <p class="text-xs text-gray-500 mb-2">Include the SDK bundle and initialize it with your project.</p>
63
+ <pre class="text-[11px] bg-gray-50 border border-gray-200 rounded p-2 whitespace-pre-wrap break-words"><code>
64
+ &lt;script src="{{ baseUrl }}/public/sdk/ui-components.iife.js"&gt;&lt;/script&gt;
65
+ &lt;script&gt;
66
+ uiCmp.init({
67
+ projectId: 'prj_yourproject',
68
+ apiKey: 'YOUR_PROJECT_API_KEY', // null for public projects
69
+ apiUrl: '{{ baseUrl }}',
70
+ });
71
+
72
+ // Alias also available
73
+ // uiComponents.init(...same config...);
74
+ &lt;/script&gt;
75
+ </code></pre>
76
+ </div>
77
+
78
+ <div>
79
+ <h3 class="font-semibold text-gray-800 mb-1">Auto-inject snippet (single script tag)</h3>
80
+ <p class="text-xs text-gray-500 mb-2">Paste this anywhere; it uses the SDK host’s origin and avoids duplicate injection.</p>
81
+ <pre class="autoInjectSnippet text-[11px] bg-gray-50 border border-gray-200 rounded p-2 whitespace-pre-wrap break-words"><code>
82
+ &lt;script&gt;
83
+ (function () {
84
+ // ---- CONFIG ----
85
+ const SCRIPT_SRC = '__ORIGIN__{{ baseUrl }}/public/sdk/ui-components.iife.js';
86
+ const INIT_OPTS = {
87
+ projectId: 'prj_yourproject',
88
+ apiKey: null, // set string for private projects
89
+ apiUrl: '{{ baseUrl }}',
90
+ };
91
+
92
+ // ---- GUARDS (idempotent) ----
93
+ if (window.__uiCmpAutoAdded) return;
94
+ window.__uiCmpAutoAdded = true;
95
+
96
+ function initUiCmp() {
97
+ if (!window.uiCmp || window.__uiCmpInitialized) return;
98
+ window.__uiCmpInitialized = true;
99
+ window.uiCmp.init(INIT_OPTS);
100
+ }
101
+
102
+ // ---- SCRIPT LOADER ----
103
+ if (window.uiCmp) {
104
+ // SDK already loaded
105
+ initUiCmp();
106
+ return;
107
+ }
108
+
109
+ // Avoid duplicate &lt;script&gt; injection
110
+ const existing = document.querySelector('script[src="' + SCRIPT_SRC + '"]');
111
+ if (existing) {
112
+ existing.addEventListener('load', initUiCmp, { once: true });
113
+ return;
114
+ }
115
+
116
+ const s = document.createElement('script');
117
+ s.src = SCRIPT_SRC;
118
+ s.async = true;
119
+ s.onload = initUiCmp;
120
+ document.head.appendChild(s);
121
+ })();
122
+ &lt;/script&gt;
123
+ </code></pre>
124
+ </div>
125
+
126
+ <div>
127
+ <h3 class="font-semibold text-gray-800 mb-1">Project setup checklist</h3>
128
+ <ul class="list-disc pl-5 space-y-1 text-xs text-gray-700">
129
+ <li>Create a project (public or private) in the Projects panel.</li>
130
+ <li>Create one or more components and assign them to the project.</li>
131
+ <li v-if="selectedProject && !selectedProject.isPublic">For private projects, copy the API key when it is shown (once) after creation or rotation.</li>
132
+ <li>Optionally configure allowed origins on the project.</li>
133
+ <li>Use the manifest endpoint or SDK to render components on your site.</li>
134
+ </ul>
135
+ </div>
136
+
137
+ <div>
138
+ <h3 class="font-semibold text-gray-800 mb-1">Troubleshooting</h3>
139
+ <ul class="list-disc pl-5 space-y-1 text-xs text-gray-700">
140
+ <li>401/403 on private projects usually means a missing or invalid <code>x-project-key</code>.</li>
141
+ <li>Check that the project is active and the component is assigned and enabled.</li>
142
+ <li>Verify that the SDK script URL matches this admin base URL ({{ baseUrl }}).</li>
143
+ </ul>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
149
+ <div class="bg-white rounded-lg shadow p-4 space-y-4">
150
+ <div class="flex items-center justify-between">
151
+ <h2 class="font-semibold text-gray-800">Projects</h2>
152
+ <button @click="refreshAll" class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200">Refresh</button>
153
+ </div>
154
+
155
+ <div class="space-y-2">
156
+ <input v-model="newProject.name" class="w-full border rounded px-3 py-2 text-sm" placeholder="Project name" />
157
+ <input v-model="newProject.projectId" class="w-full border rounded px-3 py-2 text-sm" placeholder="projectId (optional, prj_...)" />
158
+ <label class="flex items-center gap-2 text-sm">
159
+ <input type="checkbox" v-model="newProject.isPublic" />
160
+ Public
161
+ </label>
162
+ <button @click="createProject" class="w-full bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-2 rounded">Create project</button>
163
+ </div>
164
+
165
+ <div class="border-t pt-3">
166
+ <div class="text-xs text-gray-500 mb-2">Select a project:</div>
167
+ <div class="space-y-1 max-h-[420px] overflow-auto">
168
+ <button
169
+ v-for="p in projects"
170
+ :key="p.projectId"
171
+ @click="selectProject(p)"
172
+ :class="['w-full text-left px-3 py-2 rounded text-sm border', selectedProject && selectedProject.projectId === p.projectId ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:bg-gray-50']"
173
+ >
174
+ <div class="font-medium text-gray-900">{{ p.name }}</div>
175
+ <div class="text-xs text-gray-500">{{ p.projectId }} · <span :class="p.isPublic ? 'text-green-600' : 'text-amber-600'">{{ p.isPublic ? 'public' : 'private' }}</span></div>
176
+ </button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <div class="bg-white rounded-lg shadow p-4 space-y-4">
182
+ <div class="flex items-center justify-between">
183
+ <h2 class="font-semibold text-gray-800">Components</h2>
184
+ </div>
185
+
186
+ <div class="space-y-2">
187
+ <input v-model="componentEditor.code" class="w-full border rounded px-3 py-2 text-sm" placeholder="code (e.g. toast)" />
188
+ <input v-model="componentEditor.name" class="w-full border rounded px-3 py-2 text-sm" placeholder="name" />
189
+ <textarea v-model="componentEditor.html" class="w-full border rounded px-3 py-2 text-sm font-mono" rows="4" placeholder="html"></textarea>
190
+ <textarea v-model="componentEditor.css" class="w-full border rounded px-3 py-2 text-sm font-mono" rows="3" placeholder="css"></textarea>
191
+ <textarea v-model="componentEditor.js" class="w-full border rounded px-3 py-2 text-sm font-mono" rows="6" placeholder="js (return { show(){...} })"></textarea>
192
+ <textarea v-model="componentEditor.usageMarkdown" class="w-full border rounded px-3 py-2 text-sm" rows="3" placeholder="usageMarkdown"></textarea>
193
+
194
+ <div class="flex gap-2">
195
+ <button @click="saveComponent" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-2 rounded">Save</button>
196
+ <button @click="clearComponentEditor" class="px-3 py-2 rounded text-sm bg-gray-100 hover:bg-gray-200">Clear</button>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="border-t pt-3 space-y-3">
201
+ <div class="flex items-center justify-between">
202
+ <div>
203
+ <div class="font-semibold text-gray-900">AI Assist</div>
204
+ <div class="text-xs text-gray-500">Propose edits and apply into editor (manual Save).</div>
205
+ </div>
206
+ <button @click="loadLlmConfig" class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200">Reload LLM</button>
207
+ </div>
208
+
209
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
210
+ <%- include('partials/llm-provider-model-picker', {
211
+ providerInputId: 'uiComponentsAiProviderKey',
212
+ modelInputId: 'uiComponentsAiModel',
213
+ providerLabel: 'Provider',
214
+ modelLabel: 'Model',
215
+ showOpenRouterFetch: true,
216
+ }) %>
217
+ </div>
218
+
219
+ <div>
220
+ <label class="block text-xs font-medium text-gray-600 mb-1">Prompt</label>
221
+ <textarea v-model="ai.prompt" class="w-full border rounded px-2 py-2 text-sm" rows="3" placeholder="Describe the change you want..."></textarea>
222
+ </div>
223
+
224
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
225
+ <div>
226
+ <label class="block text-xs font-medium text-gray-600 mb-1">Targets</label>
227
+ <div class="flex flex-wrap gap-3 text-xs text-gray-700">
228
+ <label class="flex items-center gap-1"><input type="checkbox" v-model="ai.targets.html" /> html</label>
229
+ <label class="flex items-center gap-1"><input type="checkbox" v-model="ai.targets.css" /> css</label>
230
+ <label class="flex items-center gap-1"><input type="checkbox" v-model="ai.targets.js" /> js</label>
231
+ <label class="flex items-center gap-1"><input type="checkbox" v-model="ai.targets.usageMarkdown" /> usage</label>
232
+ </div>
233
+ </div>
234
+ <div>
235
+ <label class="block text-xs font-medium text-gray-600 mb-1">Mode</label>
236
+ <select v-model="ai.mode" class="w-full border rounded px-2 py-2 text-sm">
237
+ <option value="minimal">minimal</option>
238
+ <option value="rewrite">rewrite</option>
239
+ </select>
240
+ </div>
241
+ </div>
242
+
243
+ <div class="flex gap-2">
244
+ <button @click="aiPropose" class="flex-1 px-3 py-2 rounded text-sm bg-purple-600 hover:bg-purple-700 text-white" :disabled="aiLoading">
245
+ {{ aiLoading ? 'Proposing...' : 'Propose' }}
246
+ </button>
247
+ <button @click="aiApply" class="px-3 py-2 rounded text-sm bg-gray-800 hover:bg-gray-900 text-white" :disabled="!aiProposal">
248
+ Apply
249
+ </button>
250
+ </div>
251
+
252
+ <div v-if="aiWarnings.length" class="text-xs bg-amber-50 border border-amber-200 rounded p-2">
253
+ <div class="font-semibold text-amber-900 mb-1">Warnings</div>
254
+ <ul class="list-disc pl-5 space-y-1">
255
+ <li v-for="(w, idx) in aiWarnings" :key="idx" class="text-amber-900">{{ w }}</li>
256
+ </ul>
257
+ </div>
258
+
259
+ <div v-if="aiProposal" class="text-xs bg-gray-50 border border-gray-200 rounded p-2">
260
+ <div class="font-semibold text-gray-700 mb-1">Patch preview</div>
261
+ <pre class="whitespace-pre-wrap break-words text-[11px]">{{ aiProposal.patch }}</pre>
262
+ </div>
263
+ </div>
264
+
265
+ <div class="border-t pt-3">
266
+ <div class="text-xs text-gray-500 mb-2">Existing components:</div>
267
+ <div class="space-y-1 max-h-[320px] overflow-auto">
268
+ <button
269
+ v-for="c in components"
270
+ :key="c.code"
271
+ @click="loadComponentIntoEditor(c.code)"
272
+ class="w-full text-left px-3 py-2 rounded text-sm border border-gray-200 hover:bg-gray-50"
273
+ >
274
+ <div class="font-medium text-gray-900">{{ c.name }}</div>
275
+ <div class="text-xs text-gray-500">{{ c.code }} · v{{ c.version || 1 }}</div>
276
+ </button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="bg-white rounded-lg shadow p-4 space-y-4">
282
+ <div class="flex items-center justify-between">
283
+ <h2 class="font-semibold text-gray-800">Assignments</h2>
284
+ </div>
285
+
286
+ <div v-if="!selectedProject" class="text-sm text-gray-500">
287
+ Select a project to manage enabled components.
288
+ </div>
289
+
290
+ <div v-else class="space-y-3">
291
+ <div class="text-sm">
292
+ <div class="font-medium text-gray-900">{{ selectedProject.name }}</div>
293
+ <div class="text-xs text-gray-500">{{ selectedProject.projectId }}</div>
294
+ </div>
295
+
296
+ <div class="space-y-2">
297
+ <label class="flex items-center gap-2 text-sm">
298
+ <input type="checkbox" v-model="selectedProject.isPublic" @change="toggleProjectPublic" />
299
+ Public
300
+ </label>
301
+
302
+ <button v-if="!selectedProject.isPublic" @click="rotateKey" class="w-full text-sm px-3 py-2 rounded bg-amber-600 hover:bg-amber-700 text-white">
303
+ Rotate API Key
304
+ </button>
305
+
306
+ <div v-if="lastGeneratedKey" class="text-xs bg-gray-50 border border-gray-200 rounded p-2 break-all">
307
+ <div class="font-semibold text-gray-700 mb-1">API Key (shown once)</div>
308
+ <div class="font-mono">{{ lastGeneratedKey }}</div>
309
+ </div>
310
+
311
+ <div class="text-xs bg-gray-50 border border-gray-200 rounded p-2">
312
+ <div class="font-semibold text-gray-700 mb-1">Integration snippet</div>
313
+ <pre class="text-[11px] whitespace-pre-wrap">&lt;script src=&quot;{{ baseUrl }}/public/sdk/ui-components.iife.js&quot;&gt;&lt;/script&gt;
314
+ &lt;script&gt;
315
+ uiCmp.init({ projectId: '{{ selectedProject.projectId }}', apiKey: {{ selectedProject.isPublic ? 'null' : "'YOUR_KEY'" }}, apiUrl: '{{ baseUrl }}' });
316
+ &lt;/script&gt;</pre>
317
+ </div>
318
+ </div>
319
+
320
+ <div class="border-t pt-3">
321
+ <div class="text-xs text-gray-500 mb-2">Enable components:</div>
322
+ <div class="space-y-2 max-h-[360px] overflow-auto">
323
+ <label v-for="c in components" :key="c.code" class="flex items-center justify-between gap-3 text-sm border border-gray-200 rounded px-3 py-2">
324
+ <div>
325
+ <div class="font-medium text-gray-900">{{ c.name }}</div>
326
+ <div class="text-xs text-gray-500">{{ c.code }}</div>
327
+ </div>
328
+ <input type="checkbox" :checked="isAssigned(c.code)" @change="(e) => toggleAssignment(c.code, e.target.checked)" />
329
+ </label>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <script>
339
+ const { createApp, ref, onMounted } = Vue;
340
+
341
+ createApp({
342
+ setup() {
343
+ const baseUrl = '<%= baseUrl %>';
344
+ const adminPath = '<%= adminPath %>';
345
+ const API_BASE = window.location.origin + baseUrl;
346
+
347
+ const STORAGE_KEYS = {
348
+ providerKey: 'uiComponents.ai.providerKey',
349
+ model: 'uiComponents.ai.model',
350
+ helpOpen: 'uiComponents.help.open',
351
+ };
352
+
353
+ const toast = ref({ show: false, message: '', type: 'success' });
354
+ let toastTimer = null;
355
+
356
+ function showToast(message, type) {
357
+ toast.value = { show: true, message, type: type || 'success' };
358
+ if (toastTimer) clearTimeout(toastTimer);
359
+ toastTimer = setTimeout(() => {
360
+ toast.value.show = false;
361
+ }, 2500);
362
+ }
363
+
364
+ async function api(path, options) {
365
+ const res = await fetch(baseUrl + path, {
366
+ method: (options && options.method) || 'GET',
367
+ headers: {
368
+ 'Content-Type': 'application/json',
369
+ },
370
+ body: options && options.body ? JSON.stringify(options.body) : undefined,
371
+ });
372
+
373
+ const text = await res.text();
374
+ let data = null;
375
+ try { data = text ? JSON.parse(text) : null; } catch { data = null; }
376
+
377
+ if (!res.ok) {
378
+ const msg = (data && data.error) ? data.error : ('Request failed: ' + res.status);
379
+ throw new Error(msg);
380
+ }
381
+
382
+ return data;
383
+ }
384
+
385
+ const helpOpen = ref(false);
386
+
387
+ const projects = ref([]);
388
+ const components = ref([]);
389
+ const selectedProject = ref(null);
390
+ const assignments = ref([]);
391
+ const lastGeneratedKey = ref('');
392
+
393
+ const ai = ref({
394
+ providerKey: localStorage.getItem(STORAGE_KEYS.providerKey) || '',
395
+ model: localStorage.getItem(STORAGE_KEYS.model) || '',
396
+ prompt: '',
397
+ mode: 'minimal',
398
+ targets: { html: true, css: true, js: true, usageMarkdown: true },
399
+ });
400
+ const aiLoading = ref(false);
401
+ const aiProposal = ref(null);
402
+ const aiWarnings = ref([]);
403
+
404
+ const newProject = ref({ name: '', projectId: '', isPublic: true });
405
+
406
+ const componentEditor = ref({ code: '', name: '', html: '', js: '', css: '', usageMarkdown: '' });
407
+
408
+ function loadHelpState() {
409
+ try {
410
+ const raw = localStorage.getItem(STORAGE_KEYS.helpOpen);
411
+ if (raw === '1') helpOpen.value = true;
412
+ if (raw === '0') helpOpen.value = false;
413
+ } catch {}
414
+ }
415
+
416
+ function persistHelpState() {
417
+ try {
418
+ localStorage.setItem(STORAGE_KEYS.helpOpen, helpOpen.value ? '1' : '0');
419
+ } catch {}
420
+ }
421
+
422
+ function toggleHelp() {
423
+ helpOpen.value = !helpOpen.value;
424
+ persistHelpState();
425
+ }
426
+
427
+ function syncAiPickerToVue() {
428
+ const providerEl = document.getElementById('uiComponentsAiProviderKey');
429
+ const modelEl = document.getElementById('uiComponentsAiModel');
430
+ if (providerEl) providerEl.value = String(ai.value.providerKey || '');
431
+ if (modelEl) modelEl.value = String(ai.value.model || '');
432
+ }
433
+
434
+ function wireAiPickerListeners() {
435
+ const providerEl = document.getElementById('uiComponentsAiProviderKey');
436
+ const modelEl = document.getElementById('uiComponentsAiModel');
437
+ if (!providerEl || !modelEl) return;
438
+
439
+ if (providerEl.dataset.wired === '1') return;
440
+ providerEl.dataset.wired = '1';
441
+ modelEl.dataset.wired = '1';
442
+
443
+ providerEl.addEventListener('input', () => {
444
+ ai.value.providerKey = String(providerEl.value || '');
445
+ persistAiSettings();
446
+ });
447
+ providerEl.addEventListener('change', () => {
448
+ ai.value.providerKey = String(providerEl.value || '');
449
+ persistAiSettings();
450
+ });
451
+
452
+ modelEl.addEventListener('input', () => {
453
+ ai.value.model = String(modelEl.value || '');
454
+ persistAiSettings();
455
+ });
456
+ modelEl.addEventListener('change', () => {
457
+ ai.value.model = String(modelEl.value || '');
458
+ persistAiSettings();
459
+ });
460
+ }
461
+
462
+ async function initAiPicker() {
463
+ if (!window.__llmProviderModelPicker || !window.__llmProviderModelPicker.init) return;
464
+ await window.__llmProviderModelPicker.init({
465
+ apiBase: API_BASE,
466
+ providerInputId: 'uiComponentsAiProviderKey',
467
+ modelInputId: 'uiComponentsAiModel',
468
+ });
469
+ syncAiPickerToVue();
470
+ wireAiPickerListeners();
471
+ }
472
+
473
+ async function loadLlmConfig() {
474
+ try {
475
+ await initAiPicker();
476
+ showToast('LLM config reloaded', 'success');
477
+ } catch (e) {
478
+ showToast(e.message, 'error');
479
+ }
480
+ }
481
+
482
+ function persistAiSettings() {
483
+ try {
484
+ localStorage.setItem(STORAGE_KEYS.providerKey, String(ai.value.providerKey || ''));
485
+ localStorage.setItem(STORAGE_KEYS.model, String(ai.value.model || ''));
486
+ } catch {}
487
+ }
488
+
489
+ async function aiPropose() {
490
+ try {
491
+ const code = String(componentEditor.value.code || '').trim().toLowerCase();
492
+ if (!code) throw new Error('Select or enter a component code first');
493
+ const prompt = String(ai.value.prompt || '').trim();
494
+ if (!prompt) throw new Error('Prompt is required');
495
+
496
+ persistAiSettings();
497
+ aiLoading.value = true;
498
+ aiProposal.value = null;
499
+ aiWarnings.value = [];
500
+
501
+ const payload = {
502
+ prompt,
503
+ providerKey: ai.value.providerKey || undefined,
504
+ model: ai.value.model || undefined,
505
+ targets: ai.value.targets,
506
+ mode: ai.value.mode,
507
+ };
508
+
509
+ const data = await api('/api/admin/ui-components/ai/components/' + encodeURIComponent(code) + '/propose', {
510
+ method: 'POST',
511
+ body: payload,
512
+ });
513
+
514
+ aiProposal.value = data && data.proposal ? data.proposal : null;
515
+ aiWarnings.value = (aiProposal.value && Array.isArray(aiProposal.value.warnings)) ? aiProposal.value.warnings : [];
516
+ showToast('AI proposal ready', 'success');
517
+ } catch (e) {
518
+ showToast(e.message, 'error');
519
+ } finally {
520
+ aiLoading.value = false;
521
+ }
522
+ }
523
+
524
+ function aiApply() {
525
+ try {
526
+ if (!aiProposal.value || !aiProposal.value.fields) return;
527
+ const f = aiProposal.value.fields;
528
+ if (f.html !== undefined) componentEditor.value.html = f.html;
529
+ if (f.css !== undefined) componentEditor.value.css = f.css;
530
+ if (f.js !== undefined) componentEditor.value.js = f.js;
531
+ if (f.usageMarkdown !== undefined) componentEditor.value.usageMarkdown = f.usageMarkdown;
532
+ showToast('Applied proposal into editor (click Save to persist)', 'success');
533
+ } catch (e) {
534
+ showToast(e.message, 'error');
535
+ }
536
+ }
537
+
538
+ async function refreshProjects() {
539
+ const data = await api('/api/admin/ui-components/projects');
540
+ projects.value = (data && data.items) ? data.items : [];
541
+ }
542
+
543
+ async function refreshComponents() {
544
+ const data = await api('/api/admin/ui-components/components');
545
+ components.value = (data && data.items) ? data.items : [];
546
+ }
547
+
548
+ async function refreshAssignments(projectId) {
549
+ const data = await api('/api/admin/ui-components/projects/' + encodeURIComponent(projectId) + '/components');
550
+ assignments.value = (data && data.items) ? data.items : [];
551
+ }
552
+
553
+ async function refreshAll() {
554
+ try {
555
+ await Promise.all([refreshProjects(), refreshComponents()]);
556
+ if (selectedProject.value) {
557
+ await refreshAssignments(selectedProject.value.projectId);
558
+ }
559
+ showToast('Refreshed', 'success');
560
+ } catch (e) {
561
+ showToast(e.message, 'error');
562
+ }
563
+ }
564
+
565
+ async function createProject() {
566
+ try {
567
+ lastGeneratedKey.value = '';
568
+ const payload = {
569
+ name: newProject.value.name,
570
+ projectId: newProject.value.projectId || undefined,
571
+ isPublic: Boolean(newProject.value.isPublic),
572
+ };
573
+ const data = await api('/api/admin/ui-components/projects', { method: 'POST', body: payload });
574
+ if (data && data.apiKey) lastGeneratedKey.value = data.apiKey;
575
+ newProject.value = { name: '', projectId: '', isPublic: true };
576
+ await refreshProjects();
577
+ showToast('Project created', 'success');
578
+ } catch (e) {
579
+ showToast(e.message, 'error');
580
+ }
581
+ }
582
+
583
+ async function selectProject(p) {
584
+ try {
585
+ lastGeneratedKey.value = '';
586
+ selectedProject.value = { ...p };
587
+ await refreshAssignments(p.projectId);
588
+ } catch (e) {
589
+ showToast(e.message, 'error');
590
+ }
591
+ }
592
+
593
+ function isAssigned(code) {
594
+ return assignments.value.some((a) => a.componentCode === code && a.enabled);
595
+ }
596
+
597
+ async function toggleAssignment(code, enabled) {
598
+ try {
599
+ if (!selectedProject.value) return;
600
+ if (enabled) {
601
+ await api(
602
+ '/api/admin/ui-components/projects/' + encodeURIComponent(selectedProject.value.projectId) + '/components/' + encodeURIComponent(code),
603
+ { method: 'POST', body: { enabled: true } },
604
+ );
605
+ } else {
606
+ await api(
607
+ '/api/admin/ui-components/projects/' + encodeURIComponent(selectedProject.value.projectId) + '/components/' + encodeURIComponent(code),
608
+ { method: 'DELETE' },
609
+ );
610
+ }
611
+ await refreshAssignments(selectedProject.value.projectId);
612
+ showToast('Updated assignment', 'success');
613
+ } catch (e) {
614
+ showToast(e.message, 'error');
615
+ }
616
+ }
617
+
618
+ async function toggleProjectPublic() {
619
+ try {
620
+ lastGeneratedKey.value = '';
621
+ const data = await api('/api/admin/ui-components/projects/' + encodeURIComponent(selectedProject.value.projectId), {
622
+ method: 'PUT',
623
+ body: { isPublic: Boolean(selectedProject.value.isPublic) },
624
+ });
625
+ if (data && data.apiKey) lastGeneratedKey.value = data.apiKey;
626
+ await refreshProjects();
627
+ await refreshAssignments(selectedProject.value.projectId);
628
+ showToast('Project updated', 'success');
629
+ } catch (e) {
630
+ showToast(e.message, 'error');
631
+ }
632
+ }
633
+
634
+ async function rotateKey() {
635
+ try {
636
+ lastGeneratedKey.value = '';
637
+ const data = await api('/api/admin/ui-components/projects/' + encodeURIComponent(selectedProject.value.projectId) + '/rotate-key', { method: 'POST' });
638
+ if (data && data.apiKey) lastGeneratedKey.value = data.apiKey;
639
+ showToast('Key rotated', 'success');
640
+ } catch (e) {
641
+ showToast(e.message, 'error');
642
+ }
643
+ }
644
+
645
+ function clearComponentEditor() {
646
+ componentEditor.value = { code: '', name: '', html: '', js: '', css: '', usageMarkdown: '' };
647
+ }
648
+
649
+ async function loadComponentIntoEditor(code) {
650
+ try {
651
+ const data = await api('/api/admin/ui-components/components/' + encodeURIComponent(code));
652
+ const c = data && data.item ? data.item : null;
653
+ if (!c) return;
654
+
655
+ aiProposal.value = null;
656
+ aiWarnings.value = [];
657
+
658
+ componentEditor.value = {
659
+ code: c.code || '',
660
+ name: c.name || '',
661
+ html: c.html || '',
662
+ js: c.js || '',
663
+ css: c.css || '',
664
+ usageMarkdown: c.usageMarkdown || '',
665
+ };
666
+ } catch (e) {
667
+ showToast(e.message, 'error');
668
+ }
669
+ }
670
+
671
+ async function saveComponent() {
672
+ try {
673
+ const code = String(componentEditor.value.code || '').trim().toLowerCase();
674
+ if (!code) throw new Error('code is required');
675
+ const payload = {
676
+ code,
677
+ name: componentEditor.value.name,
678
+ html: componentEditor.value.html,
679
+ js: componentEditor.value.js,
680
+ css: componentEditor.value.css,
681
+ usageMarkdown: componentEditor.value.usageMarkdown,
682
+ };
683
+
684
+ const existing = components.value.find((c) => c.code === code);
685
+ if (existing) {
686
+ await api('/api/admin/ui-components/components/' + encodeURIComponent(code), { method: 'PUT', body: payload });
687
+ } else {
688
+ await api('/api/admin/ui-components/components', { method: 'POST', body: payload });
689
+ }
690
+
691
+ await refreshComponents();
692
+ if (selectedProject.value) await refreshAssignments(selectedProject.value.projectId);
693
+ showToast('Component saved', 'success');
694
+ } catch (e) {
695
+ showToast(e.message, 'error');
696
+ }
697
+ }
698
+
699
+ onMounted(async () => {
700
+ try {
701
+ loadHelpState();
702
+ await refreshAll();
703
+ await initAiPicker();
704
+ } catch (e) {
705
+ showToast(e.message, 'error');
706
+ }
707
+ });
708
+
709
+ return {
710
+ baseUrl,
711
+ adminPath,
712
+ toast,
713
+ helpOpen,
714
+ projects,
715
+ components,
716
+ selectedProject,
717
+ assignments,
718
+ lastGeneratedKey,
719
+ newProject,
720
+ componentEditor,
721
+ toggleHelp,
722
+ ai,
723
+ aiLoading,
724
+ aiProposal,
725
+ aiWarnings,
726
+ refreshAll,
727
+ createProject,
728
+ selectProject,
729
+ isAssigned,
730
+ toggleAssignment,
731
+ toggleProjectPublic,
732
+ rotateKey,
733
+ clearComponentEditor,
734
+ loadComponentIntoEditor,
735
+ saveComponent,
736
+ };
737
+ },
738
+ }).mount('#app');
739
+ </script>
740
+ </body>
741
+ </html>