@intranefr/superbackend 1.5.0 → 1.5.2

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 (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -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 +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -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/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -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/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -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 +185 -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 +738 -0
  120. package/src/services/consoleOverride.service.js +7 -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/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -102,6 +102,15 @@
102
102
  <label class="block text-sm font-medium">Route path</label>
103
103
  <input id="seo-ai-route" class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="/marketplace" />
104
104
  </div>
105
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
106
+ <%- include('partials/llm-provider-model-picker', {
107
+ providerInputId: 'seoAiProviderKey',
108
+ modelInputId: 'seoAiModel',
109
+ providerLabel: 'Provider',
110
+ modelLabel: 'Model',
111
+ showOpenRouterFetch: true,
112
+ }) %>
113
+ </div>
105
114
  <div class="flex gap-2">
106
115
  <button class="px-3 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 text-sm" onclick="seoAiGenerateFromView()">Generate entry</button>
107
116
  <button class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm" onclick="seoAiApplyProposed()">Apply</button>
@@ -124,6 +133,15 @@
124
133
  <label class="block text-sm font-medium">Instruction</label>
125
134
  <input id="seo-ai-instruction" class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="Improve description including desktop apps" />
126
135
  </div>
136
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
137
+ <%- include('partials/llm-provider-model-picker', {
138
+ providerInputId: 'seoAiImproveProviderKey',
139
+ modelInputId: 'seoAiImproveModel',
140
+ providerLabel: 'Provider',
141
+ modelLabel: 'Model',
142
+ showOpenRouterFetch: true,
143
+ }) %>
144
+ </div>
127
145
  <div class="flex gap-2">
128
146
  <label class="inline-flex items-center gap-2 text-sm text-gray-700">
129
147
  <input id="seo-ai-robots-noindex" type="checkbox" class="rounded border-gray-300" />
@@ -219,9 +237,16 @@
219
237
  </div>
220
238
 
221
239
  <div class="mb-3">
222
- <label class="block text-sm font-medium mb-1">Model (optional override)</label>
223
- <input id="ai-model" class="w-full border rounded px-3 py-2 text-sm" placeholder="google/gemini-2.5-flash-lite" />
224
- <div class="mt-1 text-xs text-gray-600">Uses <code>seoconfig.ai.openrouter.*</code> settings with fallback to <code>ai.openrouter.*</code>. Disabled if no API key.</div>
240
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
241
+ <%- include('partials/llm-provider-model-picker', {
242
+ providerInputId: 'ogAiProviderKey',
243
+ modelInputId: 'ai-model',
244
+ providerLabel: 'Provider',
245
+ modelLabel: 'Model (optional override)',
246
+ showOpenRouterFetch: true,
247
+ }) %>
248
+ </div>
249
+ <div class="mt-1 text-xs text-gray-600">Legacy OpenRouter keys (<code>seoconfig.ai.openrouter.apiKey</code> / <code>ai.openrouter.apiKey</code>) are still supported when provider is <code>openrouter</code>.</div>
225
250
  </div>
226
251
 
227
252
  <div class="flex justify-end gap-2">
@@ -233,7 +258,15 @@
233
258
  </div>
234
259
 
235
260
  <script>
236
- const API_BASE = window.location.origin + "<%= baseUrl %>";
261
+ const API_BASE = window.location.origin + "<%= baseUrl || '' %>";
262
+
263
+ function initLlmPickers() {
264
+ if (!window.__llmProviderModelPicker || !window.__llmProviderModelPicker.init) return;
265
+
266
+ window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'seoAiProviderKey', modelInputId: 'seoAiModel' });
267
+ window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'seoAiImproveProviderKey', modelInputId: 'seoAiImproveModel' });
268
+ window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'ogAiProviderKey', modelInputId: 'ai-model' });
269
+ }
237
270
 
238
271
  function showToast(message, type = 'success') {
239
272
  const container = document.getElementById('toast-container');
@@ -409,10 +442,17 @@
409
442
  try {
410
443
  const viewPath = document.getElementById('seo-ai-view').value;
411
444
  const routePath = document.getElementById('seo-ai-route').value;
445
+ const providerKey = document.getElementById('seoAiProviderKey')?.value;
446
+ const model = document.getElementById('seoAiModel')?.value;
412
447
  const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/generate-entry`, {
413
448
  method: 'POST',
414
449
  headers: { 'Content-Type': 'application/json' },
415
- body: JSON.stringify({ viewPath, routePath }),
450
+ body: JSON.stringify({
451
+ viewPath,
452
+ routePath,
453
+ providerKey: providerKey ? String(providerKey).trim() : undefined,
454
+ model: model ? String(model).trim() : undefined,
455
+ }),
416
456
  });
417
457
  const data = await res.json().catch(() => ({}));
418
458
  if (!res.ok) throw new Error(data?.error || 'Failed to generate entry');
@@ -432,10 +472,17 @@
432
472
  try {
433
473
  const routePath = document.getElementById('seo-ai-existing-route').value;
434
474
  const instruction = document.getElementById('seo-ai-instruction').value;
475
+ const providerKey = document.getElementById('seoAiImproveProviderKey')?.value;
476
+ const model = document.getElementById('seoAiImproveModel')?.value;
435
477
  const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/improve-entry`, {
436
478
  method: 'POST',
437
479
  headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ routePath, instruction }),
480
+ body: JSON.stringify({
481
+ routePath,
482
+ instruction,
483
+ providerKey: providerKey ? String(providerKey).trim() : undefined,
484
+ model: model ? String(model).trim() : undefined,
485
+ }),
439
486
  });
440
487
  const data = await res.json().catch(() => ({}));
441
488
  if (!res.ok) throw new Error(data?.error || 'Failed to improve entry');
@@ -617,6 +664,7 @@
617
664
  document.getElementById('ai-instruction').value = '';
618
665
  document.getElementById('ai-model').value = '';
619
666
  document.getElementById('ai-modal').classList.remove('hidden');
667
+ initLlmPickers();
620
668
  }
621
669
 
622
670
  function closeAiModal() {
@@ -634,11 +682,17 @@
634
682
  const svgRaw = document.getElementById('og-svg-raw').value;
635
683
  const instruction = document.getElementById('ai-instruction').value;
636
684
  const model = document.getElementById('ai-model').value;
685
+ const providerKey = document.getElementById('ogAiProviderKey')?.value;
637
686
 
638
687
  const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/edit-svg`, {
639
688
  method: 'POST',
640
689
  headers: { 'Content-Type': 'application/json' },
641
- body: JSON.stringify({ svgRaw, instruction, model: model || undefined }),
690
+ body: JSON.stringify({
691
+ svgRaw,
692
+ instruction,
693
+ providerKey: providerKey ? String(providerKey).trim() : undefined,
694
+ model: model || undefined,
695
+ }),
642
696
  });
643
697
  const data = await res.json().catch(() => ({}));
644
698
  if (!res.ok) throw new Error(data?.error || 'AI failed');
@@ -207,17 +207,13 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
207
207
  </div>
208
208
 
209
209
  <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
210
- <div>
211
- <label class="block text-xs font-medium text-gray-600 mb-1">Provider</label>
212
- <select v-model="ai.providerKey" class="w-full border rounded px-2 py-2 text-sm">
213
- <option value="">(default)</option>
214
- <option v-for="k in llmProviderKeys" :key="k" :value="k">{{ k }}</option>
215
- </select>
216
- </div>
217
- <div>
218
- <label class="block text-xs font-medium text-gray-600 mb-1">Model</label>
219
- <input v-model="ai.model" class="w-full border rounded px-2 py-2 text-sm" placeholder="(default)" />
220
- </div>
210
+ <%- include('partials/llm-provider-model-picker', {
211
+ providerInputId: 'uiComponentsAiProviderKey',
212
+ modelInputId: 'uiComponentsAiModel',
213
+ providerLabel: 'Provider',
214
+ modelLabel: 'Model',
215
+ showOpenRouterFetch: true,
216
+ }) %>
221
217
  </div>
222
218
 
223
219
  <div>
@@ -346,6 +342,7 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
346
342
  setup() {
347
343
  const baseUrl = '<%= baseUrl %>';
348
344
  const adminPath = '<%= adminPath %>';
345
+ const API_BASE = window.location.origin + baseUrl;
349
346
 
350
347
  const STORAGE_KEYS = {
351
348
  providerKey: 'uiComponents.ai.providerKey',
@@ -393,7 +390,6 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
393
390
  const assignments = ref([]);
394
391
  const lastGeneratedKey = ref('');
395
392
 
396
- const llmProviderKeys = ref([]);
397
393
  const ai = ref({
398
394
  providerKey: localStorage.getItem(STORAGE_KEYS.providerKey) || '',
399
395
  model: localStorage.getItem(STORAGE_KEYS.model) || '',
@@ -428,11 +424,56 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
428
424
  persistHelpState();
429
425
  }
430
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
+
431
473
  async function loadLlmConfig() {
432
474
  try {
433
- const data = await api('/api/admin/llm/config');
434
- const providers = data && data.providers ? data.providers : {};
435
- llmProviderKeys.value = Object.keys(providers || {}).sort();
475
+ await initAiPicker();
476
+ showToast('LLM config reloaded', 'success');
436
477
  } catch (e) {
437
478
  showToast(e.message, 'error');
438
479
  }
@@ -658,13 +699,8 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
658
699
  onMounted(async () => {
659
700
  try {
660
701
  loadHelpState();
661
- await loadLlmConfig();
662
702
  await refreshAll();
663
- // Replace __ORIGIN__ in auto-inject snippet
664
- const snippetEl = document.querySelector('.autoInjectSnippet code');
665
- if (snippetEl) {
666
- snippetEl.innerHTML = snippetEl.innerHTML.split('__ORIGIN__').join(window.location.origin);
667
- }
703
+ await initAiPicker();
668
704
  } catch (e) {
669
705
  showToast(e.message, 'error');
670
706
  }
@@ -683,14 +719,10 @@ curl "{{ baseUrl }}/api/ui-components/manifest/prj_yourproject" \
683
719
  newProject,
684
720
  componentEditor,
685
721
  toggleHelp,
686
- llmProviderKeys,
687
722
  ai,
688
723
  aiLoading,
689
724
  aiProposal,
690
725
  aiWarnings,
691
- loadLlmConfig,
692
- aiPropose,
693
- aiApply,
694
726
  refreshAll,
695
727
  createProject,
696
728
  selectProject,
@@ -220,11 +220,11 @@
220
220
  this.loadWorkflows().then(() => {
221
221
  if (workflowIdFromUrl !== 'new' && workflowIdFromUrl.length > 5) {
222
222
  const wf = this.workflows.find(w => w._id === workflowIdFromUrl);
223
- if (wf) this.editWorkflow(wf); else fetch(`/saas/api/workflows/${workflowIdFromUrl}`).then(res => res.ok && res.json().then(data => this.editWorkflow(data)));
223
+ if (wf) this.editWorkflow(wf); else fetch(`/saas/api/admin/workflows/${workflowIdFromUrl}`).then(res => res.ok && res.json().then(data => this.editWorkflow(data)));
224
224
  } else if (workflowIdFromUrl === 'new') this.createNew();
225
225
  });
226
226
  },
227
- async loadWorkflows() { const res = await fetch('/saas/api/workflows'); this.workflows = await res.json(); },
227
+ async loadWorkflows() { const res = await fetch('/saas/api/admin/workflows'); this.workflows = await res.json(); },
228
228
  createNew() { this.workflow = { name: 'New Workflow', status: 'inactive', entrypoint: { awaitResponse: false, allowedMethods: ['POST', 'GET'], auth: { type: 'none' } }, testDataset: { method: 'POST', body: { message: 'Hello' }, query: {}, headers: {} }, nodes: [] }; this.view = 'editor'; },
229
229
  editWorkflow(wf) {
230
230
  const currentView = this.view; this.workflow = JSON.parse(JSON.stringify(wf)); this.showRuns = false; this.runs = []; this.inspectedRun = null;
@@ -233,7 +233,7 @@
233
233
  this.view = (currentView === 'list') ? 'editor' : currentView;
234
234
  },
235
235
  toggleMethod(m) { this.workflow.entrypoint.allowedMethods = this.workflow.entrypoint.allowedMethods || []; const idx = this.workflow.entrypoint.allowedMethods.indexOf(m); if (idx > -1) this.workflow.entrypoint.allowedMethods.splice(idx, 1); else this.workflow.entrypoint.allowedMethods.push(m); },
236
- async deleteWorkflow(id) { if (confirm('Are you sure?')) { await fetch(`/saas/api/workflows/${id}`, { method: 'DELETE' }); await this.loadWorkflows(); } },
236
+ async deleteWorkflow(id) { if (confirm('Are you sure?')) { await fetch(`/saas/api/admin/workflows/${id}`, { method: 'DELETE' }); await this.loadWorkflows(); } },
237
237
  getWebhookUrl() { return window.location.origin + '/saas/w/' + (this.workflow._id || 'NEW'); },
238
238
  copyWebhookUrl() { navigator.clipboard.writeText(this.getWebhookUrl()); this.showToast('Copied!'); },
239
239
  addNode(type) { this.workflow.nodes.push({ id: 'node_' + Date.now(), type, name: 'node_' + (this.workflow.nodes.length + 1) }); },
@@ -247,7 +247,7 @@
247
247
  getNodeIcon(type) { return { llm: 'ti-brain', if: 'ti-git-branch', http: 'ti-world', exit: 'ti-door-exit', parallel: 'ti-git-merge' }[type] || 'ti-circle'; },
248
248
  getNodeColor(type) { return { llm: 'bg-indigo-500', if: 'bg-yellow-500', http: 'bg-blue-500', exit: 'bg-red-500', parallel: 'bg-purple-500' }[type]; },
249
249
  async saveWorkflow(silent = false) {
250
- const res = await fetch(this.workflow._id ? `/saas/api/workflows/${this.workflow._id}` : '/saas/api/workflows', { method: this.workflow._id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.workflow) });
250
+ const res = await fetch(this.workflow._id ? `/saas/api/admin/workflows/${this.workflow._id}` : '/saas/api/admin/workflows', { method: this.workflow._id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.workflow) });
251
251
  if (res.ok) { const saved = await res.json(); if (!silent) this.showToast('Saved!'); await this.loadWorkflows(); this.editWorkflow(saved); }
252
252
  },
253
253
  async runFullTest(entrypointOnly = false) {
@@ -255,7 +255,7 @@
255
255
  try {
256
256
  const payload = { method: this.testConfig.method, body: JSON.parse(this.testConfig.body), query: JSON.parse(this.testConfig.query), headers: JSON.parse(this.testConfig.headers) };
257
257
  if (entrypointOnly) { this.workflow.testDataset = payload; await this.saveWorkflow(true); return this.showToast('Dataset set.'); }
258
- this.testing = true; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
258
+ this.testing = true; const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
259
259
  this.testResult = await res.json(); if (this.showRuns) this.loadRuns(); this.showToast('Ran.');
260
260
  } catch (e) { this.showToast('Failed', 'error'); } finally { this.testing = false; }
261
261
  },
@@ -263,11 +263,11 @@
263
263
  async testIsolatedNode(node) {
264
264
  const context = { entrypoint: this.workflow.testDataset, payload: this.workflow.testDataset, lastNode: this.workflow.testDataset };
265
265
  try {
266
- const res = await fetch(`/saas/api/workflows/${this.workflow._id}/nodes/${node.id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, node }) });
266
+ const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/nodes/${node.id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, node }) });
267
267
  const data = await res.json(); node.testOutput = data.result; node.testResult = data.result; this.showToast('Tested');
268
268
  } catch (e) { this.showToast('Failed', 'error'); }
269
269
  },
270
- async loadRuns() { if (!this.workflow._id) return; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/runs`); this.runs = await res.json(); },
270
+ async loadRuns() { if (!this.workflow._id) return; const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/runs`); this.runs = await res.json(); },
271
271
  inspectRun(run) { this.inspectedRun = run; }, testWorkflow() { this.showTestPanel = true; },
272
272
  showToast(message, type = 'success') {
273
273
  const id = Date.now(); this.toasts.push({ id, message, type, show: true });