@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
@@ -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);