@intranefr/superbackend 1.5.3 → 1.6.3

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 (106) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +7 -0
  12. package/package.json +3 -1
  13. package/plugins/core-waiting-list-migration/README.md +118 -0
  14. package/plugins/core-waiting-list-migration/index.js +438 -0
  15. package/plugins/global-settings-presets/index.js +20 -0
  16. package/plugins/hello-cli/index.js +17 -0
  17. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  18. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  19. package/plugins/ui-components-seeder/index.js +31 -0
  20. package/public/js/admin-ui-components-preview.js +281 -0
  21. package/public/js/admin-ui-components.js +408 -0
  22. package/public/js/llm-provider-model-picker.js +193 -0
  23. package/public/test-iframe-fix.html +63 -0
  24. package/public/test-iframe.html +14 -0
  25. package/src/admin/endpointRegistry.js +68 -0
  26. package/src/controllers/admin.controller.js +25 -5
  27. package/src/controllers/adminDataCleanup.controller.js +45 -0
  28. package/src/controllers/adminLlm.controller.js +0 -8
  29. package/src/controllers/adminLogin.controller.js +269 -0
  30. package/src/controllers/adminPlugins.controller.js +55 -0
  31. package/src/controllers/adminRegistry.controller.js +106 -0
  32. package/src/controllers/adminStats.controller.js +4 -4
  33. package/src/controllers/registry.controller.js +32 -0
  34. package/src/controllers/waitingList.controller.js +52 -74
  35. package/src/middleware/auth.js +71 -1
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +454 -153
  38. package/src/models/GlobalSetting.js +11 -1
  39. package/src/models/UiComponent.js +2 -0
  40. package/src/models/User.js +1 -1
  41. package/src/routes/admin.routes.js +3 -3
  42. package/src/routes/adminAgents.routes.js +2 -2
  43. package/src/routes/adminAssets.routes.js +11 -11
  44. package/src/routes/adminBlog.routes.js +2 -2
  45. package/src/routes/adminBlogAi.routes.js +2 -2
  46. package/src/routes/adminBlogAutomation.routes.js +2 -2
  47. package/src/routes/adminCache.routes.js +2 -2
  48. package/src/routes/adminConsoleManager.routes.js +2 -2
  49. package/src/routes/adminCrons.routes.js +2 -2
  50. package/src/routes/adminDataCleanup.routes.js +26 -0
  51. package/src/routes/adminDbBrowser.routes.js +2 -2
  52. package/src/routes/adminEjsVirtual.routes.js +2 -2
  53. package/src/routes/adminFeatureFlags.routes.js +6 -6
  54. package/src/routes/adminHeadless.routes.js +2 -2
  55. package/src/routes/adminHealthChecks.routes.js +2 -2
  56. package/src/routes/adminI18n.routes.js +2 -2
  57. package/src/routes/adminJsonConfigs.routes.js +8 -8
  58. package/src/routes/adminLlm.routes.js +8 -8
  59. package/src/routes/adminLogin.routes.js +23 -0
  60. package/src/routes/adminMarkdowns.routes.js +3 -9
  61. package/src/routes/adminMigration.routes.js +12 -12
  62. package/src/routes/adminPages.routes.js +2 -2
  63. package/src/routes/adminPlugins.routes.js +15 -0
  64. package/src/routes/adminProxy.routes.js +2 -2
  65. package/src/routes/adminRateLimits.routes.js +8 -8
  66. package/src/routes/adminRbac.routes.js +2 -2
  67. package/src/routes/adminRegistry.routes.js +24 -0
  68. package/src/routes/adminScripts.routes.js +2 -2
  69. package/src/routes/adminSeoConfig.routes.js +10 -10
  70. package/src/routes/adminTelegram.routes.js +2 -2
  71. package/src/routes/adminTerminals.routes.js +2 -2
  72. package/src/routes/adminUiComponents.routes.js +2 -2
  73. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  74. package/src/routes/blogInternal.routes.js +2 -2
  75. package/src/routes/experiments.routes.js +2 -2
  76. package/src/routes/formsAdmin.routes.js +6 -6
  77. package/src/routes/globalSettings.routes.js +8 -8
  78. package/src/routes/internalExperiments.routes.js +2 -2
  79. package/src/routes/notificationAdmin.routes.js +7 -7
  80. package/src/routes/orgAdmin.routes.js +16 -16
  81. package/src/routes/pages.routes.js +3 -3
  82. package/src/routes/registry.routes.js +11 -0
  83. package/src/routes/stripeAdmin.routes.js +12 -12
  84. package/src/routes/userAdmin.routes.js +7 -7
  85. package/src/routes/waitingListAdmin.routes.js +2 -2
  86. package/src/routes/workflows.routes.js +3 -3
  87. package/src/services/dataCleanup.service.js +286 -0
  88. package/src/services/jsonConfigs.service.js +262 -0
  89. package/src/services/plugins.service.js +348 -0
  90. package/src/services/registry.service.js +452 -0
  91. package/src/services/uiComponents.service.js +180 -0
  92. package/src/services/waitingListJson.service.js +401 -0
  93. package/src/utils/rbac/rightsRegistry.js +118 -0
  94. package/test-access.js +63 -0
  95. package/test-iframe-fix.html +63 -0
  96. package/test-iframe.html +14 -0
  97. package/views/admin-403.ejs +92 -0
  98. package/views/admin-dashboard-home.ejs +52 -2
  99. package/views/admin-dashboard.ejs +143 -2
  100. package/views/admin-data-cleanup.ejs +357 -0
  101. package/views/admin-login.ejs +286 -0
  102. package/views/admin-plugins-system.ejs +223 -0
  103. package/views/admin-ui-components.ejs +82 -402
  104. package/views/admin-users.ejs +207 -11
  105. package/views/partials/dashboard/nav-items.ejs +2 -0
  106. package/views/partials/llm-provider-model-picker.ejs +0 -161
@@ -135,9 +135,12 @@
135
135
  <div>
136
136
  <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
137
137
  <select id="edit-role" class="w-full border rounded px-3 py-2">
138
- <option value="user">user</option>
139
- <option value="admin">admin</option>
138
+ <option value="">Select a role</option>
140
139
  </select>
140
+ <div id="edit-role-info" class="mt-2 p-2 bg-gray-50 rounded text-sm hidden">
141
+ <div id="edit-role-description" class="text-gray-600"></div>
142
+ <div id="edit-role-grants" class="text-gray-500 text-xs mt-1"></div>
143
+ </div>
141
144
  </div>
142
145
  <div>
143
146
  <label class="block text-sm font-medium text-gray-700 mb-1">Plan</label>
@@ -257,10 +260,15 @@
257
260
  </div>
258
261
  <div>
259
262
  <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
260
- <select id="register-role" class="w-full border rounded px-3 py-2">
261
- <option value="user">User</option>
262
- <option value="admin">Admin</option>
263
- </select>
263
+ <div class="space-y-2">
264
+ <select id="register-role" class="w-full border rounded px-3 py-2">
265
+ <option value="">Loading roles...</option>
266
+ </select>
267
+ <div id="role-info" class="hidden text-xs text-gray-600 bg-gray-50 p-2 rounded border">
268
+ <div id="role-description"></div>
269
+ <div id="role-grants" class="mt-1"></div>
270
+ </div>
271
+ </div>
264
272
  </div>
265
273
  <div id="register-error" class="hidden text-red-600 text-sm"></div>
266
274
  </form>
@@ -301,10 +309,55 @@
301
309
  }
302
310
 
303
311
  const state = { offset: 0, limit: 50, total: 0 };
312
+ const isIframe = <%= typeof isIframe !== 'undefined' && isIframe %>;
313
+
314
+ // Enhanced fetch function for iframe communication
315
+ async function apiFetch(url, options = {}) {
316
+ if (isIframe) {
317
+ // In iframe mode, request data from parent window
318
+ return new Promise((resolve, reject) => {
319
+ const messageId = 'api-request-' + Date.now() + '-' + Math.random();
320
+
321
+ const handleMessage = (event) => {
322
+ if (event.data.type === 'api-response' && event.data.messageId === messageId) {
323
+ window.removeEventListener('message', handleMessage);
324
+ if (event.data.error) {
325
+ reject(new Error(event.data.error));
326
+ } else {
327
+ resolve({
328
+ ok: event.data.ok,
329
+ json: async () => event.data.data,
330
+ text: async () => JSON.stringify(event.data.data)
331
+ });
332
+ }
333
+ }
334
+ };
335
+
336
+ window.addEventListener('message', handleMessage);
337
+
338
+ // Request data from parent
339
+ window.parent.postMessage({
340
+ type: 'api-request',
341
+ messageId: messageId,
342
+ url: url,
343
+ options: options
344
+ }, '*');
345
+
346
+ // Timeout after 10 seconds
347
+ setTimeout(() => {
348
+ window.removeEventListener('message', handleMessage);
349
+ reject(new Error('Timeout waiting for parent response'));
350
+ }, 10000);
351
+ });
352
+ } else {
353
+ // Normal mode - direct fetch
354
+ return fetch(url, options);
355
+ }
356
+ }
304
357
 
305
358
  async function loadStats() {
306
359
  try {
307
- const res = await fetch(`${API_BASE}/api/admin/users/stats`);
360
+ const res = await apiFetch(`${API_BASE}/api/admin/users/stats`);
308
361
  const data = await res.json();
309
362
  if (res.ok) {
310
363
  document.getElementById('stat-total').textContent = data.total ?? '-';
@@ -331,7 +384,7 @@
331
384
 
332
385
  try {
333
386
  const url = `${API_BASE}/api/admin/users${qs({ q, role, subscriptionStatus, currentPlan, limit: state.limit, offset: state.offset })}`;
334
- const res = await fetch(url);
387
+ const res = await apiFetch(url);
335
388
  const data = await res.json();
336
389
 
337
390
  if (!res.ok) { showToast(data?.error || 'Failed to load users', 'error'); return; }
@@ -433,13 +486,19 @@
433
486
  } catch (e) { showToast(e.message, 'error'); }
434
487
  }
435
488
 
436
- function openEditModal(userId, name, role, plan, subscription) {
489
+ async function openEditModal(userId, name, role, plan, subscription) {
437
490
  document.getElementById('edit-user-id').value = userId;
438
491
  document.getElementById('edit-name').value = name || '';
439
- document.getElementById('edit-role').value = role || 'user';
440
492
  document.getElementById('edit-plan').value = plan || 'free';
441
493
  document.getElementById('edit-subscription').value = subscription || 'none';
442
494
 
495
+ // Load RBAC roles for edit form
496
+ await loadEditRbacRoles();
497
+
498
+ // Set the role after loading RBAC roles
499
+ document.getElementById('edit-role').value = role || 'user';
500
+ displayEditRoleInfo(role || 'user');
501
+
443
502
  // Reset password fields
444
503
  document.getElementById('edit-reset-password').checked = false;
445
504
  document.getElementById('password-fields').classList.add('hidden');
@@ -625,13 +684,141 @@
625
684
  } catch (e) { showToast(e.message, 'error'); }
626
685
  }
627
686
 
687
+ // RBAC Role Management Functions
688
+ let rbacRoles = [];
689
+
690
+ async function loadRbacRoles() {
691
+ try {
692
+ const res = await fetch(`${API_BASE}/api/admin/rbac/roles`);
693
+ const data = await res.json();
694
+ if (res.ok) {
695
+ rbacRoles = data.roles || [];
696
+ populateRoleSelect();
697
+ } else {
698
+ console.error('Failed to load RBAC roles:', data.error);
699
+ // Fallback to hardcoded options
700
+ rbacRoles = [
701
+ { key: 'user', name: 'User', description: 'Regular user with no admin access' },
702
+ { key: 'admin', name: 'Admin', description: 'Admin with panel access' }
703
+ ];
704
+ populateRoleSelect();
705
+ }
706
+ } catch (error) {
707
+ console.error('Error loading RBAC roles:', error);
708
+ // Fallback to hardcoded options
709
+ rbacRoles = [
710
+ { key: 'user', name: 'User', description: 'Regular user with no admin access' },
711
+ { key: 'admin', name: 'Admin', description: 'Admin with panel access' }
712
+ ];
713
+ populateRoleSelect();
714
+ }
715
+ }
716
+
717
+ function populateRoleSelect() {
718
+ const select = document.getElementById('register-role');
719
+ select.innerHTML = '<option value="">Select a role</option>';
720
+
721
+ rbacRoles.forEach(role => {
722
+ const option = document.createElement('option');
723
+ option.value = role.key;
724
+ option.textContent = `${role.name} - ${role.description}`;
725
+ select.appendChild(option);
726
+ });
727
+ }
728
+
729
+ function displayRoleInfo(roleKey) {
730
+ const role = rbacRoles.find(r => r.key === roleKey);
731
+ const infoDiv = document.getElementById('role-info');
732
+ const descDiv = document.getElementById('role-description');
733
+ const grantsDiv = document.getElementById('role-grants');
734
+
735
+ if (!role) {
736
+ infoDiv.classList.add('hidden');
737
+ return;
738
+ }
739
+
740
+ descDiv.textContent = role.description || '';
741
+
742
+ // Show grants information if available
743
+ if (role.grants && role.grants.length > 0) {
744
+ grantsDiv.innerHTML = `<strong>Permissions:</strong> ${role.grants.slice(0, 3).join(', ')}${role.grants.length > 3 ? '...' : ''}`;
745
+ } else {
746
+ grantsDiv.innerHTML = '<strong>Permissions:</strong> Admin panel access';
747
+ }
748
+
749
+ infoDiv.classList.remove('hidden');
750
+ }
751
+
752
+ // Edit Form RBAC Functions
753
+ async function loadEditRbacRoles() {
754
+ try {
755
+ const res = await fetch(`${API_BASE}/api/admin/rbac/roles`);
756
+ const data = await res.json();
757
+ if (res.ok) {
758
+ rbacRoles = data.roles || [];
759
+ populateEditRoleSelect();
760
+ } else {
761
+ rbacRoles = [
762
+ { key: 'user', name: 'User', description: 'Regular user with no admin access' },
763
+ { key: 'admin', name: 'Admin', description: 'Admin Panel Access - Full administrative privileges' }
764
+ ];
765
+ populateEditRoleSelect();
766
+ }
767
+ } catch (error) {
768
+ rbacRoles = [
769
+ { key: 'user', name: 'User', description: 'Regular user with no admin access' },
770
+ { key: 'admin', name: 'Admin', description: 'Admin Panel Access - Full administrative privileges' }
771
+ ];
772
+ populateEditRoleSelect();
773
+ }
774
+ }
775
+
776
+ function populateEditRoleSelect() {
777
+ const select = document.getElementById('edit-role');
778
+ select.innerHTML = '<option value="">Select a role</option>';
779
+
780
+ rbacRoles.forEach(role => {
781
+ const option = document.createElement('option');
782
+ option.value = role.key;
783
+ option.textContent = `${role.name} - ${role.description}`;
784
+ select.appendChild(option);
785
+ });
786
+ }
787
+
788
+ function displayEditRoleInfo(roleKey) {
789
+ const role = rbacRoles.find(r => r.key === roleKey);
790
+ const infoDiv = document.getElementById('edit-role-info');
791
+ const descDiv = document.getElementById('edit-role-description');
792
+ const grantsDiv = document.getElementById('edit-role-grants');
793
+
794
+ if (!role) {
795
+ infoDiv.classList.add('hidden');
796
+ return;
797
+ }
798
+
799
+ descDiv.textContent = role.description || '';
800
+
801
+ // Show grants information if available
802
+ if (role.grants && role.grants.length > 0) {
803
+ grantsDiv.innerHTML = `<strong>Permissions:</strong> ${role.grants.slice(0, 3).join(', ')}${role.grants.length > 3 ? '...' : ''}`;
804
+ } else {
805
+ grantsDiv.innerHTML = '<strong>Permissions:</strong> Admin panel access';
806
+ }
807
+
808
+ infoDiv.classList.remove('hidden');
809
+ }
810
+
628
811
  function openRegisterModal() {
629
812
  document.getElementById('register-email').value = '';
630
813
  document.getElementById('register-password').value = '';
631
814
  document.getElementById('register-name').value = '';
632
- document.getElementById('register-role').value = 'user';
815
+ document.getElementById('register-role').value = '';
633
816
  document.getElementById('register-error').classList.add('hidden');
817
+ document.getElementById('role-info').classList.add('hidden');
634
818
  document.getElementById('modal-register').classList.remove('hidden');
819
+
820
+ // Load RBAC roles when modal opens
821
+ loadRbacRoles();
635
822
  }
636
823
 
637
824
  function closeRegisterModal() {
@@ -866,6 +1053,15 @@
866
1053
  document.getElementById('btn-register-submit').onclick = registerUser;
867
1054
  document.getElementById('toggle-password').onclick = togglePasswordVisibility;
868
1055
 
1056
+ // Role selection change events
1057
+ document.getElementById('register-role').onchange = (e) => {
1058
+ displayRoleInfo(e.target.value);
1059
+ };
1060
+
1061
+ document.getElementById('edit-role').onchange = (e) => {
1062
+ displayEditRoleInfo(e.target.value);
1063
+ };
1064
+
869
1065
  // Form submission
870
1066
  document.getElementById('register-form').onsubmit = (e) => {
871
1067
  e.preventDefault();
@@ -38,12 +38,14 @@
38
38
  title: 'System & DevOps',
39
39
  items: [
40
40
  { id: 'settings', label: 'Global Settings', path: adminPath + '/global-settings', icon: 'ti-settings' },
41
+ { id: 'plugins-system', label: 'Plugins system', path: adminPath + '/plugins-system', icon: 'ti-puzzle' },
41
42
  { id: 'flags', label: 'Feature Flags', path: adminPath + '/feature-flags', icon: 'ti-flag' },
42
43
  { id: 'ejs', label: 'Virtual EJS', path: adminPath + '/ejs-virtual', icon: 'ti-code' },
43
44
  { id: 'rate-limiter', label: 'Rate Limiter', path: adminPath + '/rate-limiter', icon: 'ti-traffic-cone' },
44
45
  { id: 'proxy', label: 'Proxy system', path: adminPath + '/proxy', icon: 'ti-world' },
45
46
  { id: 'cache', label: 'Cache Layer', path: adminPath + '/cache', icon: 'ti-database' },
46
47
  { id: 'db-browser', label: 'DB Browser', path: adminPath + '/db-browser', icon: 'ti-database-search' },
48
+ { id: 'data-cleanup', label: 'Data cleanup', path: adminPath + '/data-cleanup', icon: 'ti-broom' },
47
49
  { id: 'migration', label: 'Migration', path: adminPath + '/migration', icon: 'ti-database-export' },
48
50
  { id: 'webhooks', label: 'Webhooks', path: adminPath + '/webhooks', icon: 'ti-webhook' },
49
51
  { id: 'coolify', label: 'Coolify Deploy', path: adminPath + '/coolify-deploy', icon: 'ti-rocket' },
@@ -20,164 +20,3 @@
20
20
  <input id="<%= modelInputId %>" class="w-full border rounded px-2 py-2 text-sm" placeholder="e.g. google/gemini-2.5-flash-lite" list="<%= modelInputId %>__datalist" />
21
21
  <datalist id="<%= modelInputId %>__datalist"></datalist>
22
22
  </div>
23
-
24
- <script>
25
- (function () {
26
- if (!window.__llmProviderModelPicker) {
27
- window.__llmProviderModelPicker = { instances: {} };
28
- }
29
-
30
- function safeJsonParse(raw, fallback) {
31
- try {
32
- return JSON.parse(raw);
33
- } catch (_) {
34
- return fallback;
35
- }
36
- }
37
-
38
- async function fetchJson(url) {
39
- const res = await fetch(url);
40
- const data = await res.json();
41
- if (!res.ok) {
42
- throw new Error(data?.error || 'Request failed');
43
- }
44
- return data;
45
- }
46
-
47
- function setDatalistOptions(datalistEl, items) {
48
- datalistEl.innerHTML = '';
49
- const uniq = Array.from(new Set((items || []).filter(Boolean)));
50
- for (const item of uniq) {
51
- const opt = document.createElement('option');
52
- opt.value = String(item);
53
- datalistEl.appendChild(opt);
54
- }
55
- }
56
-
57
- function trim(v) {
58
- return String(v || '').trim();
59
- }
60
-
61
- function isOpenRouterProvider({ providerKey, providerConfig }) {
62
- const pk = String(providerKey || '').trim().toLowerCase();
63
- if (pk === 'openrouter') return true;
64
-
65
- const baseUrl = providerConfig && typeof providerConfig === 'object'
66
- ? String(providerConfig.baseUrl || providerConfig.baseURL || '').trim().toLowerCase()
67
- : '';
68
-
69
- return Boolean(baseUrl && baseUrl.includes('openrouter'));
70
- }
71
-
72
- function getInstanceKey({ providerInputId, modelInputId }) {
73
- return `${String(providerInputId || '').trim()}::${String(modelInputId || '').trim()}`;
74
- }
75
-
76
- function getOrCreateInstance(opts) {
77
- const key = getInstanceKey(opts);
78
- const existing = window.__llmProviderModelPicker.instances[key];
79
- if (existing) return existing;
80
-
81
- const inst = {
82
- apiBase: opts.apiBase,
83
- providerInputId: opts.providerInputId,
84
- modelInputId: opts.modelInputId,
85
- providers: {},
86
- providerModels: {},
87
- };
88
-
89
- window.__llmProviderModelPicker.instances[key] = inst;
90
- return inst;
91
- }
92
-
93
- async function loadConfig(inst) {
94
- const data = await fetchJson(`${inst.apiBase}/api/admin/llm/config`);
95
- inst.providers = data.providers || {};
96
- inst.providerModels = data.providerModels || {};
97
- return data;
98
- }
99
-
100
- function renderProviderOptions(inst) {
101
- const providerInput = document.getElementById(inst.providerInputId);
102
- const providerList = document.getElementById(`${inst.providerInputId}__datalist`);
103
- if (!providerInput || !providerList) return;
104
-
105
- const providerKeys = Object.keys(inst.providers || {}).sort();
106
- setDatalistOptions(providerList, providerKeys);
107
- }
108
-
109
- function renderModelOptions(inst) {
110
- const providerInput = document.getElementById(inst.providerInputId);
111
- const modelList = document.getElementById(`${inst.modelInputId}__datalist`);
112
- if (!providerInput || !modelList) return;
113
-
114
- const providerKey = trim(providerInput.value);
115
- const models = providerKey && inst.providerModels && typeof inst.providerModels === 'object'
116
- ? inst.providerModels[providerKey]
117
- : null;
118
-
119
- setDatalistOptions(modelList, Array.isArray(models) ? models : []);
120
- }
121
-
122
- async function maybeAutoFetchOpenRouterModels(inst) {
123
- try {
124
- const providerInput = document.getElementById(inst.providerInputId);
125
- if (!providerInput) return;
126
-
127
- const providerKey = trim(providerInput.value);
128
- const providerConfig = inst.providers && typeof inst.providers === 'object' ? inst.providers[providerKey] : null;
129
- if (!isOpenRouterProvider({ providerKey, providerConfig })) return;
130
-
131
- const existing = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels.openrouter : null;
132
- if (Array.isArray(existing) && existing.length > 0) return;
133
-
134
- await fetchOpenRouterModels({
135
- apiBase: inst.apiBase,
136
- providerInputId: inst.providerInputId,
137
- modelInputId: inst.modelInputId,
138
- });
139
- } catch {
140
- // ignore
141
- }
142
- }
143
-
144
- async function fetchOpenRouterModels(opts) {
145
- const inst = getOrCreateInstance(opts || {});
146
- inst.apiBase = (opts && opts.apiBase) || inst.apiBase || window.__llmProviderModelPicker.defaultApiBase || null;
147
- if (!inst.apiBase) return;
148
-
149
- const data = await fetchJson(`${inst.apiBase}/api/admin/llm/openrouter/models`);
150
- const models = Array.isArray(data?.models) ? data.models : [];
151
-
152
- inst.providerModels = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels : {};
153
- inst.providerModels.openrouter = models;
154
- renderModelOptions(inst);
155
- }
156
-
157
- async function init(opts) {
158
- const inst = getOrCreateInstance(opts || {});
159
-
160
- if (opts && opts.apiBase) {
161
- window.__llmProviderModelPicker.defaultApiBase = opts.apiBase;
162
- }
163
-
164
- await loadConfig(inst);
165
- renderProviderOptions(inst);
166
- renderModelOptions(inst);
167
- await maybeAutoFetchOpenRouterModels(inst);
168
-
169
- const providerInput = document.getElementById(inst.providerInputId);
170
- if (providerInput) {
171
- providerInput.addEventListener('change', async () => {
172
- renderModelOptions(inst);
173
- await maybeAutoFetchOpenRouterModels(inst);
174
- });
175
- providerInput.addEventListener('input', () => renderModelOptions(inst));
176
- }
177
- }
178
-
179
- window.__llmProviderModelPicker.init = init;
180
- window.__llmProviderModelPicker.fetchOpenRouterModels = fetchOpenRouterModels;
181
- window.__llmProviderModelPicker._util = { safeJsonParse };
182
- })();
183
- </script>