@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
@@ -113,6 +113,17 @@
113
113
  <div class="text-xs text-gray-600 mb-1">To</div>
114
114
  <input id="entryAiTo" class="border rounded px-3 py-2" readonly />
115
115
  </div>
116
+ <div class="flex-1">
117
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
118
+ <%- include('partials/llm-provider-model-picker', {
119
+ providerInputId: 'entryAiProviderKey',
120
+ modelInputId: 'entryAiModel',
121
+ providerLabel: 'Provider',
122
+ modelLabel: 'Model',
123
+ showOpenRouterFetch: true,
124
+ }) %>
125
+ </div>
126
+ </div>
116
127
  <div class="flex gap-2">
117
128
  <button type="button" onclick="aiTranslateEntry()" class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700">AI translate</button>
118
129
  </div>
@@ -147,8 +158,15 @@
147
158
  <input id="aiFilter" class="w-full border rounded px-3 py-2" placeholder="Filter keys..." oninput="renderAiCandidates()" />
148
159
  </div>
149
160
  <div>
150
- <label class="block text-sm font-medium mb-2">Model</label>
151
- <input id="aiModel" class="w-full border rounded px-3 py-2" placeholder="(optional)" />
161
+ <div class="grid grid-cols-1 gap-2">
162
+ <%- include('partials/llm-provider-model-picker', {
163
+ providerInputId: 'aiProviderKey',
164
+ modelInputId: 'aiModel',
165
+ providerLabel: 'Provider',
166
+ modelLabel: 'Model',
167
+ showOpenRouterFetch: true,
168
+ }) %>
169
+ </div>
152
170
  </div>
153
171
  <div class="flex items-center gap-2 pt-8">
154
172
  <input id="aiMissing" type="checkbox" onchange="renderAiCandidates()" />
@@ -204,7 +222,7 @@
204
222
  </div>
205
223
 
206
224
  <script>
207
- const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
225
+ const API_BASE = window.location.origin + "<%= baseUrl || '' %>";
208
226
  let allLocales = [];
209
227
  let aiResults = [];
210
228
  let aiCandidates = [];
@@ -213,6 +231,22 @@
213
231
  let aiToLocaleKeySet = null;
214
232
  let aiToLocaleKeySetFor = null;
215
233
 
234
+ function initLlmPickers() {
235
+ if (!window.__llmProviderModelPicker || !window.__llmProviderModelPicker.init) return;
236
+
237
+ window.__llmProviderModelPicker.init({
238
+ apiBase: API_BASE,
239
+ providerInputId: 'aiProviderKey',
240
+ modelInputId: 'aiModel',
241
+ });
242
+
243
+ window.__llmProviderModelPicker.init({
244
+ apiBase: API_BASE,
245
+ providerInputId: 'entryAiProviderKey',
246
+ modelInputId: 'entryAiModel',
247
+ });
248
+ }
249
+
216
250
  function showToast(message, type = 'success') {
217
251
  const container = document.getElementById('toast-container');
218
252
  const toast = document.createElement('div');
@@ -323,6 +357,7 @@
323
357
  document.getElementById('entryLocale').disabled = false;
324
358
 
325
359
  document.getElementById('modal').classList.remove('hidden');
360
+ initLlmPickers();
326
361
  }
327
362
 
328
363
  function createMissing(key) {
@@ -341,6 +376,7 @@
341
376
  document.getElementById('entryLocale').disabled = true;
342
377
 
343
378
  document.getElementById('modal').classList.remove('hidden');
379
+ initLlmPickers();
344
380
  }
345
381
 
346
382
  async function aiTranslateEntry() {
@@ -348,11 +384,19 @@
348
384
  const fromLocale = document.getElementById('entryAiFrom').value;
349
385
  const toLocale = document.getElementById('entryAiTo').value || document.getElementById('entryLocale').value;
350
386
  const text = document.getElementById('entryValue').value;
387
+ const providerKey = document.getElementById('entryAiProviderKey')?.value;
388
+ const model = document.getElementById('entryAiModel')?.value;
351
389
 
352
390
  const res = await fetch(`${API_BASE}/api/admin/i18n/ai/translate-text`, {
353
391
  method: 'POST',
354
392
  headers: { 'Content-Type': 'application/json' },
355
- body: JSON.stringify({ fromLocale, toLocale, text })
393
+ body: JSON.stringify({
394
+ fromLocale,
395
+ toLocale,
396
+ text,
397
+ providerKey: providerKey ? String(providerKey).trim() : undefined,
398
+ model: model ? String(model).trim() : undefined,
399
+ })
356
400
  });
357
401
 
358
402
  if (!res.ok) {
@@ -390,6 +434,7 @@
390
434
  document.getElementById('entryLocale').disabled = true;
391
435
 
392
436
  document.getElementById('modal').classList.remove('hidden');
437
+ initLlmPickers();
393
438
  } catch (e) {
394
439
  showToast(e.message, 'error');
395
440
  }
@@ -489,6 +534,7 @@
489
534
  document.getElementById('aiCandidateSummary').textContent = '0 selected';
490
535
 
491
536
  document.getElementById('aiModal').classList.remove('hidden');
537
+ initLlmPickers();
492
538
  loadAiCandidates();
493
539
  }
494
540
 
@@ -634,6 +680,7 @@
634
680
  try {
635
681
  const fromLocale = document.getElementById('aiFrom').value;
636
682
  const toLocale = document.getElementById('aiTo').value;
683
+ const providerKey = document.getElementById('aiProviderKey')?.value;
637
684
  const model = document.getElementById('aiModel').value;
638
685
  const keys = Array.from(aiSelectedKeys);
639
686
 
@@ -645,7 +692,14 @@
645
692
  const res = await fetch(`${API_BASE}/api/admin/i18n/ai/preview`, {
646
693
  method: 'POST',
647
694
  headers: { 'Content-Type': 'application/json' },
648
- body: JSON.stringify({ fromLocale, toLocale, keys, missingOnly: false, model: model || undefined })
695
+ body: JSON.stringify({
696
+ fromLocale,
697
+ toLocale,
698
+ keys,
699
+ missingOnly: false,
700
+ providerKey: providerKey ? String(providerKey).trim() : undefined,
701
+ model: model ? String(model).trim() : undefined,
702
+ })
649
703
  });
650
704
 
651
705
  if (!res.ok) {
@@ -34,6 +34,47 @@
34
34
  </div>
35
35
 
36
36
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
37
+ <!-- Defaults + Model Suggestions -->
38
+ <div class="bg-white rounded-lg shadow">
39
+ <div class="px-4 py-3 border-b">
40
+ <h2 class="text-lg font-semibold text-gray-900">Centralized Defaults</h2>
41
+ <p class="text-sm text-gray-600">Global and per-system defaults used by AI features (provider/model). Models are suggestions only.</p>
42
+ </div>
43
+ <div class="p-4 space-y-4">
44
+ <div class="flex items-center justify-end">
45
+ <button id="btnSaveCentralDefaults" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">Save centralized defaults</button>
46
+ </div>
47
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
48
+ <div>
49
+ <label class="block text-xs font-medium mb-1">Global default providerKey</label>
50
+ <input id="defaultsProviderKey" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="e.g. openrouter" />
51
+ </div>
52
+ <div>
53
+ <label class="block text-xs font-medium mb-1">Global default model</label>
54
+ <input id="defaultsModel" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="e.g. google/gemini-2.5-flash-lite" />
55
+ </div>
56
+ </div>
57
+
58
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
59
+ <div>
60
+ <div class="flex items-center justify-between mb-1">
61
+ <label class="block text-xs font-medium">System defaults (JSON map)</label>
62
+ </div>
63
+ <textarea id="systemDefaultsJson" rows="8" class="w-full border rounded px-2 py-1 text-sm font-mono" placeholder="{\n &quot;pageBuilder.blocks.generate&quot;: { &quot;providerKey&quot;: &quot;openrouter&quot;, &quot;model&quot;: &quot;...&quot; }\n}"></textarea>
64
+ <p class="text-[11px] text-gray-500 mt-1">Stored as <code>llm.systemDefaults</code>.</p>
65
+ </div>
66
+ <div>
67
+ <div class="flex items-center justify-between mb-1">
68
+ <label class="block text-xs font-medium">Provider model suggestions (JSON map)</label>
69
+ <button id="btnFetchOpenRouterModels" class="px-2 py-1 bg-gray-200 text-gray-800 rounded text-xs">Fetch OpenRouter models</button>
70
+ </div>
71
+ <textarea id="providerModelsJson" rows="8" class="w-full border rounded px-2 py-1 text-sm font-mono" placeholder="{\n &quot;openrouter&quot;: [\"google/gemini-2.5-flash-lite\"]\n}"></textarea>
72
+ <p class="text-[11px] text-gray-500 mt-1">Stored as <code>llm.providerModels</code>. Suggestions only.</p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
37
78
  <!-- Providers + Prompts -->
38
79
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
39
80
  <!-- Providers -->
@@ -388,6 +429,9 @@ console.log(result.content);</pre>
388
429
  const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
389
430
  let providers = {};
390
431
  let prompts = {};
432
+ let defaults = { providerKey: '', model: '' };
433
+ let systemDefaults = {};
434
+ let providerModels = {};
391
435
  let auditPage = 1;
392
436
  let costPage = 1;
393
437
 
@@ -479,6 +523,14 @@ console.log(result.content);</pre>
479
523
  if (!res.ok) throw new Error(data?.error || 'Failed to load config');
480
524
  providers = data.providers || {};
481
525
  prompts = data.prompts || {};
526
+ defaults = data.defaults || { providerKey: '', model: '' };
527
+ systemDefaults = data.systemDefaults || {};
528
+ providerModels = data.providerModels || {};
529
+
530
+ document.getElementById('defaultsProviderKey').value = defaults.providerKey || '';
531
+ document.getElementById('defaultsModel').value = defaults.model || '';
532
+ document.getElementById('systemDefaultsJson').value = JSON.stringify(systemDefaults || {}, null, 2);
533
+ document.getElementById('providerModelsJson').value = JSON.stringify(providerModels || {}, null, 2);
482
534
  renderProviders();
483
535
  renderPrompts();
484
536
  } catch (e) {
@@ -488,10 +540,31 @@ console.log(result.content);</pre>
488
540
 
489
541
  async function saveConfig() {
490
542
  try {
543
+ defaults = {
544
+ providerKey: document.getElementById('defaultsProviderKey').value.trim(),
545
+ model: document.getElementById('defaultsModel').value.trim(),
546
+ };
547
+
548
+ try {
549
+ const rawSystemDefaults = document.getElementById('systemDefaultsJson').value;
550
+ systemDefaults = rawSystemDefaults && rawSystemDefaults.trim() ? JSON.parse(rawSystemDefaults) : {};
551
+ } catch (e) {
552
+ showToast('Invalid systemDefaults JSON: ' + e.message, 'error');
553
+ return;
554
+ }
555
+
556
+ try {
557
+ const rawProviderModels = document.getElementById('providerModelsJson').value;
558
+ providerModels = rawProviderModels && rawProviderModels.trim() ? JSON.parse(rawProviderModels) : {};
559
+ } catch (e) {
560
+ showToast('Invalid providerModels JSON: ' + e.message, 'error');
561
+ return;
562
+ }
563
+
491
564
  const res = await fetch(`${API_BASE}/api/admin/llm/config`, {
492
565
  method: 'POST',
493
566
  headers: { 'Content-Type': 'application/json' },
494
- body: JSON.stringify({ providers, prompts }),
567
+ body: JSON.stringify({ providers, prompts, defaults, systemDefaults, providerModels }),
495
568
  });
496
569
  const data = await res.json();
497
570
  if (!res.ok) throw new Error(data?.error || 'Failed to save config');
@@ -501,6 +574,28 @@ console.log(result.content);</pre>
501
574
  }
502
575
  }
503
576
 
577
+ async function fetchOpenRouterModels() {
578
+ try {
579
+ const res = await fetch(`${API_BASE}/api/admin/llm/openrouter/models`);
580
+ const data = await res.json();
581
+ if (!res.ok) throw new Error(data?.error || 'Failed to fetch OpenRouter models');
582
+ const models = Array.isArray(data?.models) ? data.models : [];
583
+
584
+ const rawProviderModels = document.getElementById('providerModelsJson').value;
585
+ let map = {};
586
+ try {
587
+ map = rawProviderModels && rawProviderModels.trim() ? JSON.parse(rawProviderModels) : {};
588
+ } catch (_) {
589
+ map = {};
590
+ }
591
+ map.openrouter = models;
592
+ document.getElementById('providerModelsJson').value = JSON.stringify(map, null, 2);
593
+ showToast('OpenRouter models loaded');
594
+ } catch (e) {
595
+ showToast(e.message, 'error');
596
+ }
597
+ }
598
+
504
599
  function applyProviderPresetDefaults() {
505
600
  const preset = document.getElementById('providerPreset').value;
506
601
  const baseUrlInput = document.getElementById('providerBaseUrl');
@@ -848,6 +943,9 @@ console.log(result.content);</pre>
848
943
  document.getElementById('btnSaveProvider').addEventListener('click', createOrUpdateProviderFromForm);
849
944
  document.getElementById('providerPreset').addEventListener('change', applyProviderPresetDefaults);
850
945
 
946
+ document.getElementById('btnFetchOpenRouterModels').addEventListener('click', fetchOpenRouterModels);
947
+ document.getElementById('btnSaveCentralDefaults').addEventListener('click', saveConfig);
948
+
851
949
  document.getElementById('btnAddPrompt').addEventListener('click', () => openPromptEditor(null));
852
950
  document.getElementById('btnCancelPrompt').addEventListener('click', closePromptEditor);
853
951
  document.getElementById('btnSavePrompt').addEventListener('click', createOrUpdatePromptFromForm);
@@ -142,6 +142,30 @@
142
142
  <button id="btn-members-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200" disabled>Refresh</button>
143
143
  </div>
144
144
 
145
+ <!-- Assign User Section -->
146
+ <div class="bg-gray-50 rounded-lg border p-4 mt-4">
147
+ <h4 class="font-semibold text-gray-900">Assign User to Organization</h4>
148
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-3">
149
+ <div>
150
+ <label class="block text-sm font-medium text-gray-700 mb-1">User *</label>
151
+ <select id="assign-user-select" class="w-full border rounded px-3 py-2">
152
+ <option value="">Loading users...</option>
153
+ </select>
154
+ </div>
155
+ <div>
156
+ <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
157
+ <select id="assign-user-role" class="w-full border rounded px-3 py-2">
158
+ <option value="member">member</option>
159
+ <option value="admin">admin</option>
160
+ <option value="viewer">viewer</option>
161
+ </select>
162
+ </div>
163
+ <div class="flex items-end">
164
+ <button id="btn-assign-user" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" disabled>Assign User</button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
145
169
  <div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-4">
146
170
  <div>
147
171
  <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
@@ -453,8 +477,138 @@
453
477
  orgs: { offset: 0, limit: 25, total: 0, selectedOrgId: null },
454
478
  members: { offset: 0, limit: 50, total: 0 },
455
479
  invites: { offset: 0, limit: 50, total: 0 },
480
+ availableUsers: [],
456
481
  };
457
482
 
483
+ async function loadAvailableUsers() {
484
+
485
+ console.log('Loading available users...');
486
+
487
+ const orgId = state.orgs.selectedOrgId;
488
+ if (!orgId) {
489
+ console.log('No org selected');
490
+ return;
491
+ }
492
+
493
+ try {
494
+ // Load all users
495
+ const usersRes = await fetch(`${API_BASE}/api/admin/users`, {
496
+ headers: { 'Accept': 'application/json' },
497
+ credentials: 'same-origin'
498
+ });
499
+
500
+ const usersData = await usersRes.json();
501
+
502
+ if (!usersRes.ok) {
503
+ console.error('Failed to load users:', usersData?.error);
504
+ const select = document.getElementById('assign-user-select');
505
+ if (select) {
506
+ select.innerHTML = `<option value="">Error: ${usersData?.error || 'Unknown error'}</option>`;
507
+ }
508
+ return;
509
+ }else{
510
+ console.log('Loaded users:', usersData);
511
+ }
512
+
513
+ // Load current org members
514
+ const membersRes = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members?limit=500`, {
515
+ headers: { 'Accept': 'application/json' },
516
+ credentials: 'same-origin'
517
+ });
518
+ const membersData = await membersRes.json();
519
+
520
+ if (!membersRes.ok) {
521
+ console.error('Failed to load members:', membersData?.error);
522
+ return;
523
+ }
524
+
525
+ const allUsers = Array.isArray(usersData?.users) ? usersData.users : [];
526
+ const currentMembers = Array.isArray(membersData?.members) ? membersData.members : [];
527
+ const memberUserIds = new Set(currentMembers.map(m => String(m.userId)));
528
+
529
+ // Filter out users who are already members
530
+ const availableUsers = allUsers.filter(user => !memberUserIds.has(String(user._id)));
531
+ state.availableUsers = availableUsers;
532
+
533
+ // Update the select dropdown
534
+ const select = document.getElementById('assign-user-select');
535
+ if (select) {
536
+ select.innerHTML = '<option value="">Select a user...</option>';
537
+ availableUsers.forEach(user => {
538
+ const option = document.createElement('option');
539
+ option.value = user._id;
540
+ option.textContent = `${user.name || 'No name'} (${user.email})`;
541
+ select.appendChild(option);
542
+ });
543
+ }
544
+
545
+ // Enable/disable assign button based on availability
546
+ const assignBtn = document.getElementById('btn-assign-user');
547
+ if (assignBtn) {
548
+ assignBtn.disabled = availableUsers.length === 0;
549
+ if (availableUsers.length === 0) {
550
+ select.innerHTML = '<option value="">No available users</option>';
551
+ }
552
+ }
553
+ } catch (e) {
554
+ console.error('Failed to load available users:', e);
555
+ const select = document.getElementById('assign-user-select');
556
+ if (select) {
557
+ select.innerHTML = '<option value="">Failed to load users</option>';
558
+ }
559
+ }
560
+ }
561
+
562
+ async function assignUserToOrg() {
563
+ const orgId = state.orgs.selectedOrgId;
564
+ const userId = document.getElementById('assign-user-select')?.value;
565
+ const role = document.getElementById('assign-user-role')?.value;
566
+
567
+ if (!orgId) {
568
+ showToast('No organization selected', 'error');
569
+ return;
570
+ }
571
+
572
+ if (!userId) {
573
+ showToast('Please select a user', 'error');
574
+ return;
575
+ }
576
+
577
+ if (!role) {
578
+ showToast('Please select a role', 'error');
579
+ return;
580
+ }
581
+
582
+ const userName = state.availableUsers.find(u => u._id === userId)?.name || userId;
583
+
584
+ try {
585
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members`, {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
588
+ credentials: 'same-origin',
589
+ body: JSON.stringify({ userId, role }),
590
+ });
591
+
592
+ const data = await res.json();
593
+
594
+ if (!res.ok) {
595
+ showToast(data?.error || 'Failed to assign user', 'error');
596
+ return;
597
+ }
598
+
599
+ showToast(data?.message || 'User assigned successfully', 'success');
600
+
601
+ // Reset form
602
+ document.getElementById('assign-user-select').value = '';
603
+ document.getElementById('assign-user-role').value = 'member';
604
+
605
+ // Reload members list and available users
606
+ await Promise.all([loadMembers(), loadAvailableUsers()]);
607
+ } catch (e) {
608
+ showToast(e.message || 'Failed to assign user', 'error');
609
+ }
610
+ }
611
+
458
612
  async function loadOrgs() {
459
613
  const q = document.getElementById('orgs-q')?.value?.trim();
460
614
  const status = document.getElementById('orgs-status')?.value?.trim();
@@ -539,6 +693,7 @@
539
693
  </td>
540
694
  <td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
541
695
  <button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}" data-description="${escapeHtml(o?.description || '')}" data-owner="${escapeHtml(o?.ownerUserId)}" data-status="${escapeHtml(o?.status)}">Edit</button>
696
+ <button class="text-gray-600 hover:text-gray-800 mr-2" data-copy="${escapeHtml(o?._id)}" title="Copy orgId">Copy ID</button>
542
697
  ${o?.status === 'active'
543
698
  ? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
544
699
  : `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
@@ -567,6 +722,7 @@
567
722
  const ids = [
568
723
  'btn-selected-refresh',
569
724
  'btn-members-refresh',
725
+ 'btn-assign-user',
570
726
  'btn-members-apply',
571
727
  'btn-members-reset',
572
728
  'btn-members-prev',
@@ -591,7 +747,7 @@
591
747
  state.invites.offset = 0;
592
748
 
593
749
  setSelectedControlsEnabled(true);
594
- await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
750
+ await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites(), loadAvailableUsers()]);
595
751
  await loadOrgs();
596
752
  }
597
753
 
@@ -1039,6 +1195,9 @@
1039
1195
  if (e.target.matches('[data-edit]')) {
1040
1196
  const btn = e.target;
1041
1197
  openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
1198
+ } else if (e.target.matches('[data-copy]')) {
1199
+ const btn = e.target;
1200
+ copyText(btn.dataset.copy);
1042
1201
  } else if (e.target.matches('[data-disable]')) {
1043
1202
  const btn = e.target;
1044
1203
  disableOrganization(btn.dataset.disable, btn.dataset.name);
@@ -1057,6 +1216,9 @@
1057
1216
  const membersRefreshBtn = document.getElementById('btn-members-refresh');
1058
1217
  if (membersRefreshBtn) membersRefreshBtn.onclick = () => loadMembers();
1059
1218
 
1219
+ const assignUserBtn = document.getElementById('btn-assign-user');
1220
+ if (assignUserBtn) assignUserBtn.onclick = () => assignUserToOrg();
1221
+
1060
1222
  const membersApplyBtn = document.getElementById('btn-members-apply');
1061
1223
  if (membersApplyBtn) membersApplyBtn.onclick = () => { state.members.offset = 0; loadMembers(); };
1062
1224